diff options
author | Samson <16504129+sagudev@users.noreply.github.com> | 2024-05-01 17:47:11 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-05-01 15:47:11 +0000 |
commit | eb7484de5ea71be0d9c24eeddad82f5744d7ce25 (patch) | |
tree | e9ba6e656947c63e34889e9c1e0bfaa7e1f0b520 /tests | |
parent | 6065abcb6bfcc28ad1995349b6f16f6752d0f051 (diff) | |
download | servo-eb7484de5ea71be0d9c24eeddad82f5744d7ce25.tar.gz servo-eb7484de5ea71be0d9c24eeddad82f5744d7ce25.zip |
Update web-platform-tests to revision b'86de4ffa4e439098e05f05de7d8cae1c24ff84fb' (#32200)
Co-authored-by: Servo WPT Sync <josh+wptsync@joshmatthews.net>
Diffstat (limited to 'tests')
893 files changed, 44827 insertions, 6557 deletions
diff --git a/tests/wpt/meta-legacy-layout/css/css-animations/parsing/animation-delay-end-computed.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-animations/parsing/animation-delay-end-computed.tentative.html.ini deleted file mode 100644 index f4ec3b330f0..00000000000 --- a/tests/wpt/meta-legacy-layout/css/css-animations/parsing/animation-delay-end-computed.tentative.html.ini +++ /dev/null @@ -1,12 +0,0 @@ -[animation-delay-end-computed.tentative.html] - [Property animation-delay-end value 'initial'] - expected: FAIL - - [Property animation-delay-end value '-500ms'] - expected: FAIL - - [Property animation-delay-end value 'calc(2 * 3s)'] - expected: FAIL - - [Property animation-delay-end value '20s, 10s'] - expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-animations/parsing/animation-delay-end-valid.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-animations/parsing/animation-delay-end-valid.tentative.html.ini deleted file mode 100644 index 0809ede9423..00000000000 --- a/tests/wpt/meta-legacy-layout/css/css-animations/parsing/animation-delay-end-valid.tentative.html.ini +++ /dev/null @@ -1,12 +0,0 @@ -[animation-delay-end-valid.tentative.html] - [e.style['animation-delay-end'\] = "-5ms" should set the property value] - expected: FAIL - - [e.style['animation-delay-end'\] = "0s" should set the property value] - expected: FAIL - - [e.style['animation-delay-end'\] = "10s" should set the property value] - expected: FAIL - - [e.style['animation-delay-end'\] = "20s, 10s" should set the property value] - expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-animations/parsing/animation-delay-shorthand-computed.html.ini b/tests/wpt/meta-legacy-layout/css/css-animations/parsing/animation-delay-shorthand-computed.html.ini deleted file mode 100644 index 1967721ce03..00000000000 --- a/tests/wpt/meta-legacy-layout/css/css-animations/parsing/animation-delay-shorthand-computed.html.ini +++ /dev/null @@ -1,60 +0,0 @@ -[animation-delay-shorthand-computed.html] - [Property animation-delay value '1s 2s'] - expected: FAIL - - [Property animation-delay value '1s 2s, 3s'] - expected: FAIL - - [Property animation-delay value '1s, 2s 3s'] - expected: FAIL - - [Property animation-delay value 'cover'] - expected: FAIL - - [Property animation-delay value 'contain'] - expected: FAIL - - [Property animation-delay value 'enter'] - expected: FAIL - - [Property animation-delay value 'exit'] - expected: FAIL - - [Property animation-delay value 'enter, exit'] - expected: FAIL - - [Property animation-delay value 'enter 0% enter 100%'] - expected: FAIL - - [Property animation-delay value 'exit 0% exit 100%'] - expected: FAIL - - [Property animation-delay value 'cover 0% cover 100%'] - expected: FAIL - - [Property animation-delay value 'contain 0% contain 100%'] - expected: FAIL - - [Property animation-delay value 'cover 50%'] - expected: FAIL - - [Property animation-delay value 'contain 50%'] - expected: FAIL - - [Property animation-delay value 'enter 50%'] - expected: FAIL - - [Property animation-delay value 'exit 50%'] - expected: FAIL - - [Property animation-delay value 'enter 50% 0s'] - expected: FAIL - - [Property animation-delay value '0s enter 50%'] - expected: FAIL - - [Property animation-delay value 'enter 50% exit 50%'] - expected: FAIL - - [Property animation-delay value 'cover 50% enter 50%, contain 50% exit 50%'] - expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-animations/parsing/animation-delay-shorthand.html.ini b/tests/wpt/meta-legacy-layout/css/css-animations/parsing/animation-delay-shorthand.html.ini deleted file mode 100644 index a67cdfc1609..00000000000 --- a/tests/wpt/meta-legacy-layout/css/css-animations/parsing/animation-delay-shorthand.html.ini +++ /dev/null @@ -1,150 +0,0 @@ -[animation-delay-shorthand.html] - [e.style['animation-delay'\] = "1s 2s" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "1s 2s, 3s" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "1s, 2s 3s" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "cover" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "contain" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "enter" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "exit" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "enter, exit" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "enter 0% enter 100%" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "exit 0% exit 100%" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "cover 0% cover 100%" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "contain 0% contain 100%" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "cover 50%" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "contain 50%" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "enter 50%" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "exit 50%" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "enter 50% 0s" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "0s enter 50%" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "enter 50% exit 50%" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "cover 50% enter 50%, contain 50% exit 50%" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "1s 2s" should set animation-delay-end] - expected: FAIL - - [e.style['animation-delay'\] = "1s 2s" should set animation-delay-start] - expected: FAIL - - [e.style['animation-delay'\] = "1s 2s" should not set unrelated longhands] - expected: FAIL - - [e.style['animation-delay'\] = "1s" should set animation-delay-end] - expected: FAIL - - [e.style['animation-delay'\] = "1s" should set animation-delay-start] - expected: FAIL - - [e.style['animation-delay'\] = "1s" should not set unrelated longhands] - expected: FAIL - - [e.style['animation-delay'\] = "cover" should set animation-delay-end] - expected: FAIL - - [e.style['animation-delay'\] = "cover" should set animation-delay-start] - expected: FAIL - - [e.style['animation-delay'\] = "cover" should not set unrelated longhands] - expected: FAIL - - [e.style['animation-delay'\] = "contain" should set animation-delay-end] - expected: FAIL - - [e.style['animation-delay'\] = "contain" should set animation-delay-start] - expected: FAIL - - [e.style['animation-delay'\] = "contain" should not set unrelated longhands] - expected: FAIL - - [e.style['animation-delay'\] = "enter 10% exit 20%" should set animation-delay-end] - expected: FAIL - - [e.style['animation-delay'\] = "enter 10% exit 20%" should set animation-delay-start] - expected: FAIL - - [e.style['animation-delay'\] = "enter 10% exit 20%" should not set unrelated longhands] - expected: FAIL - - [e.style['animation-delay'\] = "1s 2s, 3s 4s" should set animation-delay-end] - expected: FAIL - - [e.style['animation-delay'\] = "1s 2s, 3s 4s" should set animation-delay-start] - expected: FAIL - - [e.style['animation-delay'\] = "1s 2s, 3s 4s" should not set unrelated longhands] - expected: FAIL - - [e.style['animation-delay'\] = "1s 2s, 3s, 4s 5s" should set animation-delay-end] - expected: FAIL - - [e.style['animation-delay'\] = "1s 2s, 3s, 4s 5s" should set animation-delay-start] - expected: FAIL - - [e.style['animation-delay'\] = "1s 2s, 3s, 4s 5s" should not set unrelated longhands] - expected: FAIL - - [e.style['animation-delay'\] = "enter, exit" should set animation-delay-end] - expected: FAIL - - [e.style['animation-delay'\] = "enter, exit" should set animation-delay-start] - expected: FAIL - - [e.style['animation-delay'\] = "enter, exit" should not set unrelated longhands] - expected: FAIL - - [e.style['animation-delay'\] = "enter 0%, exit" should set animation-delay-end] - expected: FAIL - - [e.style['animation-delay'\] = "enter 0%, exit" should set animation-delay-start] - expected: FAIL - - [e.style['animation-delay'\] = "enter 0%, exit" should not set unrelated longhands] - expected: FAIL - - [e.style['animation-delay'\] = "enter 0% 1s, 2s exit 50%" should set animation-delay-end] - expected: FAIL - - [e.style['animation-delay'\] = "enter 0% 1s, 2s exit 50%" should set animation-delay-start] - expected: FAIL - - [e.style['animation-delay'\] = "enter 0% 1s, 2s exit 50%" should not set unrelated longhands] - expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-animations/parsing/animation-delay-start-computed.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-animations/parsing/animation-delay-start-computed.tentative.html.ini deleted file mode 100644 index 8341379e4d8..00000000000 --- a/tests/wpt/meta-legacy-layout/css/css-animations/parsing/animation-delay-start-computed.tentative.html.ini +++ /dev/null @@ -1,12 +0,0 @@ -[animation-delay-start-computed.tentative.html] - [Property animation-delay-start value 'initial'] - expected: FAIL - - [Property animation-delay-start value '-500ms'] - expected: FAIL - - [Property animation-delay-start value 'calc(2 * 3s)'] - expected: FAIL - - [Property animation-delay-start value '20s, 10s'] - expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-animations/parsing/animation-delay-start-valid.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-animations/parsing/animation-delay-start-valid.tentative.html.ini deleted file mode 100644 index 4ad3bb1e96b..00000000000 --- a/tests/wpt/meta-legacy-layout/css/css-animations/parsing/animation-delay-start-valid.tentative.html.ini +++ /dev/null @@ -1,12 +0,0 @@ -[animation-delay-start-valid.tentative.html] - [e.style['animation-delay-start'\] = "-5ms" should set the property value] - expected: FAIL - - [e.style['animation-delay-start'\] = "0s" should set the property value] - expected: FAIL - - [e.style['animation-delay-start'\] = "10s" should set the property value] - expected: FAIL - - [e.style['animation-delay-start'\] = "20s, 10s" should set the property value] - expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-cascade/scope-pseudo-element.html.ini b/tests/wpt/meta-legacy-layout/css/css-cascade/scope-pseudo-element.html.ini new file mode 100644 index 00000000000..80b96e894af --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/css-cascade/scope-pseudo-element.html.ini @@ -0,0 +1,2 @@ +[scope-pseudo-element.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-flexbox/intrinsic-size/col-wrap-020.html.ini b/tests/wpt/meta-legacy-layout/css/css-flexbox/intrinsic-size/col-wrap-020.html.ini new file mode 100644 index 00000000000..5895cfc6764 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/css-flexbox/intrinsic-size/col-wrap-020.html.ini @@ -0,0 +1,2 @@ +[col-wrap-020.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-fonts/matching/font-unicode-PUA-primary-font.html.ini b/tests/wpt/meta-legacy-layout/css/css-fonts/matching/font-unicode-PUA-primary-font.html.ini new file mode 100644 index 00000000000..4e0025de6c7 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/css-fonts/matching/font-unicode-PUA-primary-font.html.ini @@ -0,0 +1,2 @@ +[font-unicode-PUA-primary-font.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-fonts/variation-sequences.html.ini b/tests/wpt/meta-legacy-layout/css/css-fonts/variation-sequences.html.ini new file mode 100644 index 00000000000..c9053836d50 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/css-fonts/variation-sequences.html.ini @@ -0,0 +1,2 @@ +[variation-sequences.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade-002.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade-002.html.ini deleted file mode 100644 index 59c9d5fe2c7..00000000000 --- a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade-002.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[highlight-cascade-002.html] - expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade-009.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade-009.html.ini deleted file mode 100644 index ca26253afa4..00000000000 --- a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade-009.html.ini +++ /dev/null @@ -1,9 +0,0 @@ -[highlight-cascade-009.html] - [body ::selection uses its own custom property] - expected: FAIL - - [div::selection inherits a custom property] - expected: FAIL - - [div::selection uses its own custom property] - expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/cascade-highlight-001.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/cascade-highlight-001.html.ini index c89aebb5ebb..c89aebb5ebb 100644 --- a/tests/wpt/meta-legacy-layout/css/css-pseudo/cascade-highlight-001.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/cascade-highlight-001.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/cascade-highlight-002.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/cascade-highlight-002.html.ini index dd2d810e286..dd2d810e286 100644 --- a/tests/wpt/meta-legacy-layout/css/css-pseudo/cascade-highlight-002.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/cascade-highlight-002.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/cascade-highlight-004.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/cascade-highlight-004.html.ini index 5630d0776bd..5630d0776bd 100644 --- a/tests/wpt/meta-legacy-layout/css/css-pseudo/cascade-highlight-004.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/cascade-highlight-004.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/cascade-highlight-005.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/cascade-highlight-005.html.ini index e0dd6427940..e0dd6427940 100644 --- a/tests/wpt/meta-legacy-layout/css/css-pseudo/cascade-highlight-005.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/cascade-highlight-005.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade-007.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-cascade-007.html.ini index 99307641b90..99307641b90 100644 --- a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade-007.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-cascade-007.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-currentcolor-computed-inheritance.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-currentcolor-computed-inheritance.html.ini index 99456944d45..99456944d45 100644 --- a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-currentcolor-computed-inheritance.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-currentcolor-computed-inheritance.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-currentcolor-computed.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-currentcolor-computed.html.ini index d29df11928e..d29df11928e 100644 --- a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-currentcolor-computed.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-currentcolor-computed.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-currentcolor-painting-properties-001.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-001.html.ini index 3919445a963..3919445a963 100644 --- a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-currentcolor-painting-properties-001.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-001.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-currentcolor-painting-properties-002.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-002.html.ini index 44b19e7edb4..44b19e7edb4 100644 --- a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-currentcolor-painting-properties-002.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-002.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-currentcolor-painting-text-shadow-001.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-001.html.ini index 9cbcacbddd7..9cbcacbddd7 100644 --- a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-currentcolor-painting-text-shadow-001.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-001.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-currentcolor-painting-text-shadow-002.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-002.html.ini index e5976d08881..e5976d08881 100644 --- a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-currentcolor-painting-text-shadow-002.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-002.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-currentcolor-root-explicit-default-002.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-002.html.ini index ad2279b7044..ad2279b7044 100644 --- a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-currentcolor-root-explicit-default-002.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-002.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-currentcolor-root-implicit-default-001.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-implicit-default-001.html.ini index 1310b157b1a..1310b157b1a 100644 --- a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-currentcolor-root-implicit-default-001.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-implicit-default-001.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-currentcolor-root-implicit-default-002.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-implicit-default-002.html.ini index 5d7f1ec0af7..5d7f1ec0af7 100644 --- a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-currentcolor-root-implicit-default-002.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-implicit-default-002.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-paired-cascade-004.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-paired-cascade-004.html.ini index d0f4abb4abb..d0f4abb4abb 100644 --- a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-paired-cascade-004.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-paired-cascade-004.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-pseudos-computed.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-pseudos-computed.html.ini index 7c4acf428f7..7c4acf428f7 100644 --- a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-pseudos-computed.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-pseudos-computed.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-pseudos-inheritance-computed-001.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-pseudos-inheritance-computed-001.html.ini index a4e4bdc95a6..a4e4bdc95a6 100644 --- a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-pseudos-inheritance-computed-001.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-pseudos-inheritance-computed-001.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-pseudos-visited-computed-001.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-pseudos-visited-computed-001.html.ini index 267ebaa918b..267ebaa918b 100644 --- a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-pseudos-visited-computed-001.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-cascade/highlight-pseudos-visited-computed-001.html.ini diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-painting-shadows-horizontal.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-painting-shadows-horizontal.html.ini new file mode 100644 index 00000000000..b15ba58b7ca --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-painting-shadows-horizontal.html.ini @@ -0,0 +1,2 @@ +[highlight-painting-shadows-horizontal.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-painting-shadows-vertical.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-painting-shadows-vertical.html.ini new file mode 100644 index 00000000000..b39e4c85fcd --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/css-pseudo/highlight-painting-shadows-vertical.html.ini @@ -0,0 +1,2 @@ +[highlight-painting-shadows-vertical.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/target-text-dynamic-004.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/target-text-dynamic-004.html.ini deleted file mode 100644 index c8b5a4ab11b..00000000000 --- a/tests/wpt/meta-legacy-layout/css/css-pseudo/target-text-dynamic-004.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[target-text-dynamic-004.html] - expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/target-text-shadow-horizontal.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/target-text-shadow-horizontal.html.ini new file mode 100644 index 00000000000..d0ac640e63a --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/css-pseudo/target-text-shadow-horizontal.html.ini @@ -0,0 +1,2 @@ +[target-text-shadow-horizontal.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-pseudo/target-text-shadow-vertical.html.ini b/tests/wpt/meta-legacy-layout/css/css-pseudo/target-text-shadow-vertical.html.ini new file mode 100644 index 00000000000..12979318989 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/css-pseudo/target-text-shadow-vertical.html.ini @@ -0,0 +1,2 @@ +[target-text-shadow-vertical.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-text/white-space/trailing-other-space-separators-break-spaces-015.html.ini b/tests/wpt/meta-legacy-layout/css/css-text/white-space/trailing-other-space-separators-break-spaces-015.html.ini new file mode 100644 index 00000000000..2318c013698 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/css-text/white-space/trailing-other-space-separators-break-spaces-015.html.ini @@ -0,0 +1,2 @@ +[trailing-other-space-separators-break-spaces-015.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-transitions/shadow-root-insertion.html.ini b/tests/wpt/meta-legacy-layout/css/css-transitions/shadow-root-insertion.html.ini new file mode 100644 index 00000000000..bc9bbbcf84d --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/css-transitions/shadow-root-insertion.html.ini @@ -0,0 +1,3 @@ +[shadow-root-insertion.html] + [addition of a shadow root should not cancel in-flight transitions] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-values/calc-rounding-003.html.ini b/tests/wpt/meta-legacy-layout/css/css-values/calc-rounding-003.html.ini new file mode 100644 index 00000000000..b82a3cd9292 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/css-values/calc-rounding-003.html.ini @@ -0,0 +1,2 @@ +[calc-rounding-003.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-values/calc-size/animation/calc-size-height-interpolation.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-values/calc-size/animation/calc-size-height-interpolation.tentative.html.ini index c6d325ba2e1..a0c94fe0a1c 100644 --- a/tests/wpt/meta-legacy-layout/css/css-values/calc-size/animation/calc-size-height-interpolation.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-values/calc-size/animation/calc-size-height-interpolation.tentative.html.ini @@ -2764,3 +2764,123 @@ [Web Animations: property <height> from [calc-size(37px, 200px)\] to [calc-size(37px, size * 2 + 3% + 17px)\] at (1.25) should be [75px\]] expected: FAIL + + [CSS Transitions: property <height> from [calc-size(auto, size)\] to [50%\] at (-0.25) should be [87.5px\]] + expected: FAIL + + [CSS Transitions: property <height> from [calc-size(auto, size)\] to [50%\] at (0) should be [100px\]] + expected: FAIL + + [CSS Transitions: property <height> from [calc-size(auto, size)\] to [50%\] at (0.75) should be [137.5px\]] + expected: FAIL + + [CSS Transitions: property <height> from [calc-size(auto, size)\] to [50%\] at (1) should be [150px\]] + expected: FAIL + + [CSS Transitions: property <height> from [calc-size(auto, size)\] to [50%\] at (1.25) should be [162.5px\]] + expected: FAIL + + [CSS Transitions with transition: all: property <height> from [calc-size(auto, size)\] to [50%\] at (-0.25) should be [87.5px\]] + expected: FAIL + + [CSS Transitions with transition: all: property <height> from [calc-size(auto, size)\] to [50%\] at (0) should be [100px\]] + expected: FAIL + + [CSS Transitions with transition: all: property <height> from [calc-size(auto, size)\] to [50%\] at (0.75) should be [137.5px\]] + expected: FAIL + + [CSS Transitions with transition: all: property <height> from [calc-size(auto, size)\] to [50%\] at (1) should be [150px\]] + expected: FAIL + + [CSS Transitions with transition: all: property <height> from [calc-size(auto, size)\] to [50%\] at (1.25) should be [162.5px\]] + expected: FAIL + + [CSS Animations: property <height> from [calc-size(auto, size)\] to [50%\] at (-0.25) should be [87.5px\]] + expected: FAIL + + [CSS Animations: property <height> from [calc-size(auto, size)\] to [50%\] at (0) should be [100px\]] + expected: FAIL + + [CSS Animations: property <height> from [calc-size(auto, size)\] to [50%\] at (0.75) should be [137.5px\]] + expected: FAIL + + [CSS Animations: property <height> from [calc-size(auto, size)\] to [50%\] at (1) should be [150px\]] + expected: FAIL + + [CSS Animations: property <height> from [calc-size(auto, size)\] to [50%\] at (1.25) should be [162.5px\]] + expected: FAIL + + [Web Animations: property <height> from [calc-size(auto, size)\] to [50%\] at (-0.25) should be [87.5px\]] + expected: FAIL + + [Web Animations: property <height> from [calc-size(auto, size)\] to [50%\] at (0) should be [100px\]] + expected: FAIL + + [Web Animations: property <height> from [calc-size(auto, size)\] to [50%\] at (0.75) should be [137.5px\]] + expected: FAIL + + [Web Animations: property <height> from [calc-size(auto, size)\] to [50%\] at (1) should be [150px\]] + expected: FAIL + + [Web Animations: property <height> from [calc-size(auto, size)\] to [50%\] at (1.25) should be [162.5px\]] + expected: FAIL + + [CSS Transitions: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (-0.25) should be [250px\]] + expected: FAIL + + [CSS Transitions: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (0) should be [200px\]] + expected: FAIL + + [CSS Transitions: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (0.75) should be [50px\]] + expected: FAIL + + [CSS Transitions: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (1) should be [0px\]] + expected: FAIL + + [CSS Transitions: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (1.25) should be [0px\]] + expected: FAIL + + [CSS Transitions with transition: all: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (-0.25) should be [250px\]] + expected: FAIL + + [CSS Transitions with transition: all: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (0) should be [200px\]] + expected: FAIL + + [CSS Transitions with transition: all: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (0.75) should be [50px\]] + expected: FAIL + + [CSS Transitions with transition: all: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (1) should be [0px\]] + expected: FAIL + + [CSS Transitions with transition: all: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (1.25) should be [0px\]] + expected: FAIL + + [CSS Animations: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (-0.25) should be [250px\]] + expected: FAIL + + [CSS Animations: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (0) should be [200px\]] + expected: FAIL + + [CSS Animations: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (0.75) should be [50px\]] + expected: FAIL + + [CSS Animations: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (1) should be [0px\]] + expected: FAIL + + [CSS Animations: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (1.25) should be [0px\]] + expected: FAIL + + [Web Animations: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (-0.25) should be [250px\]] + expected: FAIL + + [Web Animations: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (0) should be [200px\]] + expected: FAIL + + [Web Animations: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (0.75) should be [50px\]] + expected: FAIL + + [Web Animations: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (1) should be [0px\]] + expected: FAIL + + [Web Animations: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (1.25) should be [0px\]] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-values/calc-size/calc-size-parsing.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-values/calc-size/calc-size-parsing.tentative.html.ini index 15063a73f41..998e202dbc3 100644 --- a/tests/wpt/meta-legacy-layout/css/css-values/calc-size/calc-size-parsing.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-values/calc-size/calc-size-parsing.tentative.html.ini @@ -55,3 +55,21 @@ [e.style['width'\] = "calc-size(10px, sign(size) * size)" should set the property value] expected: FAIL + + [e.style['width'\] = "calc-size(30px)" should set the property value] + expected: FAIL + + [e.style['width'\] = "calc-size(min(30px, 2em))" should set the property value] + expected: FAIL + + [e.style['width'\] = "calc-size(calc-size(any, 30px))" should set the property value] + expected: FAIL + + [e.style['width'\] = "calc-size(fit-content)" should set the property value] + expected: FAIL + + [e.style['width'\] = "calc-size(calc-size(fit-content, size * 2))" should set the property value] + expected: FAIL + + [e.style['width'\] = "calc-size(calc-size(30px))" should set the property value] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/css-values/container-progress-computed.tentative.html.ini b/tests/wpt/meta-legacy-layout/css/css-values/container-progress-computed.tentative.html.ini index e83399384fa..7bfa9700dc5 100644 --- a/tests/wpt/meta-legacy-layout/css/css-values/container-progress-computed.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/css/css-values/container-progress-computed.tentative.html.ini @@ -46,3 +46,15 @@ [calc(container-progress(width of my-container-2 from 0px to 1px) * 1deg) should be used-value-equivalent to 5051deg] expected: FAIL + + [container-progress() width fallback for non-existing container name] + expected: FAIL + + [container-progress() height fallback for non-existing container names] + expected: FAIL + + [container-progress() width fallback for out of scope container] + expected: FAIL + + [container-progress() height fallback for out of scope container] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/css/mediaqueries/prefers-color-scheme-svg-as-image.html.ini b/tests/wpt/meta-legacy-layout/css/mediaqueries/prefers-color-scheme-svg-as-image.html.ini new file mode 100644 index 00000000000..2477c232c9a --- /dev/null +++ b/tests/wpt/meta-legacy-layout/css/mediaqueries/prefers-color-scheme-svg-as-image.html.ini @@ -0,0 +1,2 @@ +[prefers-color-scheme-svg-as-image.html] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/dom/idlharness.window.js.ini b/tests/wpt/meta-legacy-layout/dom/idlharness.window.js.ini index 01cee9c7a4c..44af306d92c 100644 --- a/tests/wpt/meta-legacy-layout/dom/idlharness.window.js.ini +++ b/tests/wpt/meta-legacy-layout/dom/idlharness.window.js.ini @@ -842,6 +842,63 @@ [ShadowRoot interface: attribute clonable] expected: FAIL + [Document interface: operation prepend((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [Document interface: operation append((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [Document interface: operation replaceChildren((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [DocumentType interface: operation before((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [DocumentType interface: operation after((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [DocumentType interface: operation replaceWith((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [DocumentFragment interface: operation prepend((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [DocumentFragment interface: operation append((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [DocumentFragment interface: operation replaceChildren((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [ShadowRoot interface: attribute serializable] + expected: FAIL + + [Element interface: operation prepend((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [Element interface: operation append((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [Element interface: operation replaceChildren((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [Element interface: operation before((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [Element interface: operation after((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [Element interface: operation replaceWith((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [CharacterData interface: operation before((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [CharacterData interface: operation after((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [CharacterData interface: operation replaceWith((Node or TrustedScript or DOMString)...)] + expected: FAIL + [idlharness.window.html?include=Node] [idl_test setup] diff --git a/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/continue-css-animation-left.html.ini b/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/continue-css-animation-left.html.ini new file mode 100644 index 00000000000..f109b605d2c --- /dev/null +++ b/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/continue-css-animation-left.html.ini @@ -0,0 +1,3 @@ +[continue-css-animation-left.html] + [Node.moveBefore should preserve CSS animation state (left)] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/continue-css-animation-transform.html.ini b/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/continue-css-animation-transform.html.ini new file mode 100644 index 00000000000..05654dd2a4e --- /dev/null +++ b/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/continue-css-animation-transform.html.ini @@ -0,0 +1,3 @@ +[continue-css-animation-transform.html] + [Node.moveBefore should preserve CSS animation state (transform)] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/continue-css-transition-left-pseudo.html.ini b/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/continue-css-transition-left-pseudo.html.ini new file mode 100644 index 00000000000..a6273f52a31 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/continue-css-transition-left-pseudo.html.ini @@ -0,0 +1,4 @@ +[continue-css-transition-left-pseudo.html] + expected: TIMEOUT + [Node.moveBefore should preserve CSS transition state on pseudo-elements (left)] + expected: TIMEOUT diff --git a/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/continue-css-transition-left.html.ini b/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/continue-css-transition-left.html.ini new file mode 100644 index 00000000000..60f17eb404b --- /dev/null +++ b/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/continue-css-transition-left.html.ini @@ -0,0 +1,3 @@ +[continue-css-transition-left.html] + [Node.moveBefore should preserve CSS transition state (left)] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/continue-css-transition-transform-pseudo.html.ini b/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/continue-css-transition-transform-pseudo.html.ini new file mode 100644 index 00000000000..67a8527bd20 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/continue-css-transition-transform-pseudo.html.ini @@ -0,0 +1,4 @@ +[continue-css-transition-transform-pseudo.html] + expected: TIMEOUT + [Node.moveBefore should preserve CSS transition state on pseudo-elements (transform)] + expected: TIMEOUT diff --git a/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/continue-css-transition-transform.html.ini b/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/continue-css-transition-transform.html.ini new file mode 100644 index 00000000000..bc2827631f7 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/continue-css-transition-transform.html.ini @@ -0,0 +1,4 @@ +[continue-css-transition-transform.html] + expected: TIMEOUT + [Node.moveBefore should preserve CSS transition state (transform)] + expected: TIMEOUT diff --git a/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/css-animation-commit-styles.html.ini b/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/css-animation-commit-styles.html.ini new file mode 100644 index 00000000000..e38f3a2648b --- /dev/null +++ b/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/css-animation-commit-styles.html.ini @@ -0,0 +1,3 @@ +[css-animation-commit-styles.html] + [Calling commitStyles after Node.moveBefore should commit mid-transition value] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/css-transition-cross-document.html.ini b/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/css-transition-cross-document.html.ini new file mode 100644 index 00000000000..eccb2a04a6a --- /dev/null +++ b/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/css-transition-cross-document.html.ini @@ -0,0 +1,3 @@ +[css-transition-cross-document.html] + [Moving a transition across documents should reset its state] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/css-transition-cross-shadow.html.ini b/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/css-transition-cross-shadow.html.ini new file mode 100644 index 00000000000..a19b4f2e5d1 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/css-transition-cross-shadow.html.ini @@ -0,0 +1,3 @@ +[css-transition-cross-shadow.html] + [Moving an element with a transition across shadow boundaries should reset the transition] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/css-transition-to-disconnected-document.html.ini b/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/css-transition-to-disconnected-document.html.ini new file mode 100644 index 00000000000..dd6192bc1b6 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/css-transition-to-disconnected-document.html.ini @@ -0,0 +1,3 @@ +[css-transition-to-disconnected-document.html] + [Moving an element with a transition to a disconnected document should reset the transitionm state] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/css-transition-trigger.html.ini b/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/css-transition-trigger.html.ini new file mode 100644 index 00000000000..272163fb327 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/dom/nodes/moveBefore/tentative/css-transition-trigger.html.ini @@ -0,0 +1,3 @@ +[css-transition-trigger.html] + [Node.moveBefore should trigger CSS transition state (left) if needed] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/dom/observable/tentative/observable-every.any.js.ini b/tests/wpt/meta-legacy-layout/dom/observable/tentative/observable-every.any.js.ini new file mode 100644 index 00000000000..9c2fa7172f5 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/dom/observable/tentative/observable-every.any.js.ini @@ -0,0 +1,62 @@ +[observable-every.any.worker.html] + [every(): Promise resolves to true if all values pass the predicate] + expected: FAIL + + [every(): Promise resolves to false if any value fails the predicate] + expected: FAIL + + [every(): Abort the subscription to the source if the predicate does not pass] + expected: FAIL + + [every(): Lifecycle checks when all values pass the predicate] + expected: FAIL + + [every(): Lifecycle checks when any value fails the predicate] + expected: FAIL + + [every(): Resolves with true if the observable completes without emitting a value] + expected: FAIL + + [every(): Rejects with any error emitted from the source observable] + expected: FAIL + + [every(): Rejects with any error thrown from the predicate] + expected: FAIL + + [every(): Index is passed into the predicate] + expected: FAIL + + [every(): Rejects with a DOMException if the source Observable is aborted] + expected: FAIL + + +[observable-every.any.html] + [every(): Promise resolves to true if all values pass the predicate] + expected: FAIL + + [every(): Promise resolves to false if any value fails the predicate] + expected: FAIL + + [every(): Abort the subscription to the source if the predicate does not pass] + expected: FAIL + + [every(): Lifecycle checks when all values pass the predicate] + expected: FAIL + + [every(): Lifecycle checks when any value fails the predicate] + expected: FAIL + + [every(): Resolves with true if the observable completes without emitting a value] + expected: FAIL + + [every(): Rejects with any error emitted from the source observable] + expected: FAIL + + [every(): Rejects with any error thrown from the predicate] + expected: FAIL + + [every(): Index is passed into the predicate] + expected: FAIL + + [every(): Rejects with a DOMException if the source Observable is aborted] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/dom/observable/tentative/observable-filter.any.js.ini b/tests/wpt/meta-legacy-layout/dom/observable/tentative/observable-filter.any.js.ini index 159169b6565..64a04893c3b 100644 --- a/tests/wpt/meta-legacy-layout/dom/observable/tentative/observable-filter.any.js.ini +++ b/tests/wpt/meta-legacy-layout/dom/observable/tentative/observable-filter.any.js.ini @@ -14,6 +14,9 @@ [filter(): Upon source completion, source Observable teardown sequence happens after downstream filter complete() is called] expected: FAIL + [filter(): Index is passed correctly to predicate] + expected: FAIL + [observable-filter.any.worker.html] [filter(): Returned Observable filters out results based on predicate] @@ -30,3 +33,6 @@ [filter(): Upon source completion, source Observable teardown sequence happens after downstream filter complete() is called] expected: FAIL + + [filter(): Index is passed correctly to predicate] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/dom/observable/tentative/observable-find.any.js.ini b/tests/wpt/meta-legacy-layout/dom/observable/tentative/observable-find.any.js.ini new file mode 100644 index 00000000000..719eaa35c4a --- /dev/null +++ b/tests/wpt/meta-legacy-layout/dom/observable/tentative/observable-find.any.js.ini @@ -0,0 +1,38 @@ +[observable-find.any.worker.html] + [find(): Promise resolves with the first value that passes the predicate] + expected: FAIL + + [find(): Promise resolves with undefined if no value passes the predicate] + expected: FAIL + + [find(): Promise rejects with the error emitted from the source Observable] + expected: FAIL + + [find(): Promise rejects with any error thrown from the predicate] + expected: FAIL + + [find(): Passes the index of the value to the predicate] + expected: FAIL + + [find(): Rejects with AbortError when the signal is aborted] + expected: FAIL + + +[observable-find.any.html] + [find(): Promise resolves with the first value that passes the predicate] + expected: FAIL + + [find(): Promise resolves with undefined if no value passes the predicate] + expected: FAIL + + [find(): Promise rejects with the error emitted from the source Observable] + expected: FAIL + + [find(): Promise rejects with any error thrown from the predicate] + expected: FAIL + + [find(): Passes the index of the value to the predicate] + expected: FAIL + + [find(): Rejects with AbortError when the signal is aborted] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/dom/observable/tentative/observable-inspect.any.js.ini b/tests/wpt/meta-legacy-layout/dom/observable/tentative/observable-inspect.any.js.ini new file mode 100644 index 00000000000..467445ed86d --- /dev/null +++ b/tests/wpt/meta-legacy-layout/dom/observable/tentative/observable-inspect.any.js.ini @@ -0,0 +1,80 @@ +[observable-inspect.any.worker.html] + [inspect(): Provides a pre-subscription subscribe callback] + expected: FAIL + + [inspect(): Provides a way to tap into the values and completions of the source observable using an observer] + expected: FAIL + + [inspect(): Error handler does not stop error from being reported to the global, when subscriber does not pass error handler] + expected: FAIL + + [inspect(): Provides a way to tap into the values and errors of the source observable using an observer. Errors are passed through] + expected: FAIL + + [inspect(): ObserverCallback passed in] + expected: FAIL + + [inspect(): Throwing an error in the observer next handler is caught and sent to the error callback of the result observable] + expected: FAIL + + [inspect(): Throwing an error in the observer error handler in inspect() is caught and sent to the error callback of the result observable] + expected: FAIL + + [inspect(): Throwing an error in the observer complete handler is caught and sent to the error callback of the result observable] + expected: FAIL + + [inspect(): Throwing an error in the next handler function in do should be caught and sent to the error callback of the result observable] + expected: FAIL + + [inspect(): Errors thrown in subscribe() Inspector handler subscribe handler are caught and sent to error callback] + expected: FAIL + + [inspect(): Provides a way to tap into the moment a source observable is unsubscribed from] + expected: FAIL + + [inspect(): Inspector abort() handler is not called if the source completes before the result is unsubscribed from] + expected: FAIL + + [inspect(): Errors thrown from inspect()'s abort() handler are caught and reported to the global, because the subscription is already closed by the time the handler runs] + expected: FAIL + + +[observable-inspect.any.html] + [inspect(): Provides a pre-subscription subscribe callback] + expected: FAIL + + [inspect(): Provides a way to tap into the values and completions of the source observable using an observer] + expected: FAIL + + [inspect(): Error handler does not stop error from being reported to the global, when subscriber does not pass error handler] + expected: FAIL + + [inspect(): Provides a way to tap into the values and errors of the source observable using an observer. Errors are passed through] + expected: FAIL + + [inspect(): ObserverCallback passed in] + expected: FAIL + + [inspect(): Throwing an error in the observer next handler is caught and sent to the error callback of the result observable] + expected: FAIL + + [inspect(): Throwing an error in the observer error handler in inspect() is caught and sent to the error callback of the result observable] + expected: FAIL + + [inspect(): Throwing an error in the observer complete handler is caught and sent to the error callback of the result observable] + expected: FAIL + + [inspect(): Throwing an error in the next handler function in do should be caught and sent to the error callback of the result observable] + expected: FAIL + + [inspect(): Errors thrown in subscribe() Inspector handler subscribe handler are caught and sent to error callback] + expected: FAIL + + [inspect(): Provides a way to tap into the moment a source observable is unsubscribed from] + expected: FAIL + + [inspect(): Inspector abort() handler is not called if the source completes before the result is unsubscribed from] + expected: FAIL + + [inspect(): Errors thrown from inspect()'s abort() handler are caught and reported to the global, because the subscription is already closed by the time the handler runs] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/dom/observable/tentative/observable-some.any.js.ini b/tests/wpt/meta-legacy-layout/dom/observable/tentative/observable-some.any.js.ini new file mode 100644 index 00000000000..12776e64658 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/dom/observable/tentative/observable-some.any.js.ini @@ -0,0 +1,44 @@ +[observable-some.any.html] + [some(): subscriber is inactive after the first value that passes the predicate, because the source was unsubscribed from] + expected: FAIL + + [observable-some] + expected: FAIL + + [observable-some 1] + expected: FAIL + + [some(): The returned promise rejects with an error if the predicate errors] + expected: FAIL + + [some(): The returned promise rejects with an error if the source observable errors] + expected: FAIL + + [some(): The returned promise resolves as false if the source observable completes without emitting a value] + expected: FAIL + + [some(): The return promise rejects with a DOMException if the signal is aborted] + expected: FAIL + + +[observable-some.any.worker.html] + [some(): subscriber is inactive after the first value that passes the predicate, because the source was unsubscribed from] + expected: FAIL + + [observable-some] + expected: FAIL + + [observable-some 1] + expected: FAIL + + [some(): The returned promise rejects with an error if the predicate errors] + expected: FAIL + + [some(): The returned promise rejects with an error if the source observable errors] + expected: FAIL + + [some(): The returned promise resolves as false if the source observable completes without emitting a value] + expected: FAIL + + [some(): The return promise rejects with a DOMException if the signal is aborted] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/fetch/api/request/request-bad-port.any.js.ini b/tests/wpt/meta-legacy-layout/fetch/api/request/request-bad-port.any.js.ini index 6b37ad41c18..8cd1689b1a9 100644 --- a/tests/wpt/meta-legacy-layout/fetch/api/request/request-bad-port.any.js.ini +++ b/tests/wpt/meta-legacy-layout/fetch/api/request/request-bad-port.any.js.ini @@ -5,144 +5,5 @@ expected: ERROR [request-bad-port.any.worker.html] - expected: TIMEOUT - [Request on bad port 989 should throw TypeError.] - expected: TIMEOUT - - [Request on bad port 990 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 993 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 995 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 1719 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 1720 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 1723 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 2049 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 3659 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 4045 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 5060 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 5061 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6000 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6566 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6665 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6666 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6667 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6668 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6669 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6697 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 10080 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 4190 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6679 should throw TypeError.] - expected: NOTRUN - [request-bad-port.any.html] - expected: TIMEOUT - [Request on bad port 989 should throw TypeError.] - expected: TIMEOUT - - [Request on bad port 990 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 993 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 995 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 1719 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 1720 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 1723 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 2049 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 3659 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 4045 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 5060 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 5061 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6000 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6566 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6665 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6666 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6667 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6668 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6669 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6697 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 10080 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 4190 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6679 should throw TypeError.] - expected: NOTRUN diff --git a/tests/wpt/meta-legacy-layout/fetch/metadata/generated/appcache-manifest.https.sub.html.ini b/tests/wpt/meta-legacy-layout/fetch/metadata/generated/appcache-manifest.https.sub.html.ini deleted file mode 100644 index fdf575f99df..00000000000 --- a/tests/wpt/meta-legacy-layout/fetch/metadata/generated/appcache-manifest.https.sub.html.ini +++ /dev/null @@ -1,60 +0,0 @@ -[appcache-manifest.https.sub.html] - [sec-fetch-site - Same origin] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Cross-site] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Same site] - expected: PRECONDITION_FAILED - - [sec-fetch-site - HTTPS downgrade (header not sent)] - expected: PRECONDITION_FAILED - - [sec-fetch-site - HTTPS upgrade] - expected: PRECONDITION_FAILED - - [sec-fetch-site - HTTPS downgrade-upgrade] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Cross-Site -> Same Origin] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Cross-Site -> Same-Site] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Cross-Site -> Cross-Site] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Same-Origin -> Same Origin] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Same-Origin -> Same-Site] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Same-Origin -> Cross-Site] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Same-Site -> Same Origin] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Same-Site -> Same-Site] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Same-Site -> Cross-Site] - expected: PRECONDITION_FAILED - - [sec-fetch-mode] - expected: PRECONDITION_FAILED - - [sec-fetch-dest] - expected: PRECONDITION_FAILED - - [sec-fetch-user] - expected: PRECONDITION_FAILED diff --git a/tests/wpt/meta-legacy-layout/fetch/metadata/generated/css-images.sub.tentative.html.ini b/tests/wpt/meta-legacy-layout/fetch/metadata/generated/css-images.sub.tentative.html.ini index fb087c8dd92..f9de5391ad6 100644 --- a/tests/wpt/meta-legacy-layout/fetch/metadata/generated/css-images.sub.tentative.html.ini +++ b/tests/wpt/meta-legacy-layout/fetch/metadata/generated/css-images.sub.tentative.html.ini @@ -146,6 +146,3 @@ [list-style-image sec-fetch-site - HTTPS downgrade-upgrade] expected: FAIL - - [background-image sec-fetch-user - Not sent to non-trustworthy cross-site destination] - expected: TIMEOUT diff --git a/tests/wpt/meta-legacy-layout/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-pageshow-events-window-open.html.ini b/tests/wpt/meta-legacy-layout/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-pageshow-events-window-open.html.ini index 324db3d9b35..5aef7ce66ce 100644 --- a/tests/wpt/meta-legacy-layout/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-pageshow-events-window-open.html.ini +++ b/tests/wpt/meta-legacy-layout/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-pageshow-events-window-open.html.ini @@ -10,3 +10,6 @@ [load event does not fire on window.open('about:blank?foo')] expected: FAIL + + [load event does not fire on window.open('about:blank')] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin.window.js.ini b/tests/wpt/meta-legacy-layout/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin.window.js.ini deleted file mode 100644 index 7dc346632a4..00000000000 --- a/tests/wpt/meta-legacy-layout/html/browsers/browsing-the-web/navigating-across-documents/navigation-unload-same-origin.window.js.ini +++ /dev/null @@ -1,3 +0,0 @@ -[navigation-unload-same-origin.window.html] - [Same-origin navigation started from unload handler must be ignored] - expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/html/dom/idlharness.https.html.ini b/tests/wpt/meta-legacy-layout/html/dom/idlharness.https.html.ini index d8bed830195..322279129cc 100644 --- a/tests/wpt/meta-legacy-layout/html/dom/idlharness.https.html.ini +++ b/tests/wpt/meta-legacy-layout/html/dom/idlharness.https.html.ini @@ -2090,6 +2090,21 @@ [Element interface: operation setHTMLUnsafe(HTMLString)] expected: FAIL + [Element interface: document.createElement("noscript") must inherit property "getHTML(optional GetHTMLOptions)" with the proper type] + expected: FAIL + + [Element interface: calling getHTML(optional GetHTMLOptions) on document.createElement("noscript") with too few arguments must throw TypeError] + expected: FAIL + + [ShadowRoot interface: operation getHTML(optional GetHTMLOptions)] + expected: FAIL + + [ShadowRoot interface: attribute innerHTML] + expected: FAIL + + [Element interface: operation getHTML(optional GetHTMLOptions)] + expected: FAIL + [idlharness.https.html?include=(Document|Window)] [Document interface: documentWithHandlers must inherit property "queryCommandEnabled(DOMString)" with the proper type] @@ -5096,3 +5111,9 @@ [HTMLElement interface: document.createElement("noscript") must inherit property "writingSuggestions" with the proper type] expected: FAIL + + [HTMLTemplateElement interface: attribute shadowRootSerializable] + expected: FAIL + + [HTMLTemplateElement interface: document.createElement("template") must inherit property "shadowRootSerializable" with the proper type] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-2.html.ini b/tests/wpt/meta-legacy-layout/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-2.html.ini index 26704422bbe..68203d2a082 100644 --- a/tests/wpt/meta-legacy-layout/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-2.html.ini +++ b/tests/wpt/meta-legacy-layout/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-2.html.ini @@ -1,4 +1,4 @@ [iframe_sandbox_popups_escaping-2.html] - expected: TIMEOUT + expected: CRASH [Check that popups from a sandboxed iframe escape the sandbox if\n allow-popups-to-escape-sandbox is used] expected: TIMEOUT diff --git a/tests/wpt/meta-legacy-layout/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-3.html.ini b/tests/wpt/meta-legacy-layout/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-3.html.ini index fe55ddae3f1..a6c31bc671e 100644 --- a/tests/wpt/meta-legacy-layout/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-3.html.ini +++ b/tests/wpt/meta-legacy-layout/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-3.html.ini @@ -1,4 +1,4 @@ [iframe_sandbox_popups_escaping-3.html] type: testharness [Check that popups from a sandboxed iframe escape the sandbox if\n allow-popups-to-escape-sandbox is used] - expected: FAIL + expected: TIMEOUT diff --git a/tests/wpt/meta-legacy-layout/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_nonescaping-3.html.ini b/tests/wpt/meta-legacy-layout/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_nonescaping-3.html.ini index 653c5e98f2f..ff6467094b8 100644 --- a/tests/wpt/meta-legacy-layout/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_nonescaping-3.html.ini +++ b/tests/wpt/meta-legacy-layout/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_nonescaping-3.html.ini @@ -1,4 +1,3 @@ [iframe_sandbox_popups_nonescaping-3.html] - expected: CRASH [Check that popups from a sandboxed iframe do not escape the sandbox] - expected: NOTRUN + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/html/semantics/forms/the-input-element/input-stepdown-02.html.ini b/tests/wpt/meta-legacy-layout/html/semantics/forms/the-input-element/input-stepdown-02.html.ini new file mode 100644 index 00000000000..4df29982fe6 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/html/semantics/forms/the-input-element/input-stepdown-02.html.ini @@ -0,0 +1,3 @@ +[input-stepdown-02.html] + [stepDown() on input with no initial value and positive min value] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/mediasession/idlharness.window.js.ini b/tests/wpt/meta-legacy-layout/mediasession/idlharness.window.js.ini index 3a0fc287fff..17abd82b61c 100644 --- a/tests/wpt/meta-legacy-layout/mediasession/idlharness.window.js.ini +++ b/tests/wpt/meta-legacy-layout/mediasession/idlharness.window.js.ini @@ -22,3 +22,36 @@ [MediaSession interface: calling setCameraActive(boolean) on navigator.mediaSession with too few arguments must throw TypeError] expected: FAIL + + [MediaMetadata interface: attribute chapterInfo] + expected: FAIL + + [MediaMetadata interface: new MediaMetadata() must inherit property "chapterInfo" with the proper type] + expected: FAIL + + [ChapterInformation interface: existence and properties of interface object] + expected: FAIL + + [ChapterInformation interface object length] + expected: FAIL + + [ChapterInformation interface object name] + expected: FAIL + + [ChapterInformation interface: existence and properties of interface prototype object] + expected: FAIL + + [ChapterInformation interface: existence and properties of interface prototype object's "constructor" property] + expected: FAIL + + [ChapterInformation interface: existence and properties of interface prototype object's @@unscopables property] + expected: FAIL + + [ChapterInformation interface: attribute title] + expected: FAIL + + [ChapterInformation interface: attribute startTime] + expected: FAIL + + [ChapterInformation interface: attribute artwork] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/streams/readable-streams/async-iterator.any.js.ini b/tests/wpt/meta-legacy-layout/streams/readable-streams/async-iterator.any.js.ini index 0126c346abb..deaa654bfdd 100644 --- a/tests/wpt/meta-legacy-layout/streams/readable-streams/async-iterator.any.js.ini +++ b/tests/wpt/meta-legacy-layout/streams/readable-streams/async-iterator.any.js.ini @@ -137,6 +137,12 @@ [close() while next() is pending] expected: FAIL + [return(); next() with delayed cancel()] + expected: FAIL + + [return(); next() with delayed cancel() [no awaiting\]] + expected: FAIL + [async-iterator.any.worker.html] [Async-iterating an empty but not closed/errored stream never executes the loop body and stalls the async function] @@ -271,6 +277,12 @@ [close() while next() is pending] expected: FAIL + [return(); next() with delayed cancel()] + expected: FAIL + + [return(); next() with delayed cancel() [no awaiting\]] + expected: FAIL + [async-iterator.any.shadowrealm.html] expected: ERROR diff --git a/tests/wpt/meta-legacy-layout/wasm/jsapi/idlharness.any.js.ini b/tests/wpt/meta-legacy-layout/wasm/jsapi/idlharness.any.js.ini index 44353fa582a..954c0406b7b 100644 --- a/tests/wpt/meta-legacy-layout/wasm/jsapi/idlharness.any.js.ini +++ b/tests/wpt/meta-legacy-layout/wasm/jsapi/idlharness.any.js.ini @@ -11,6 +11,18 @@ [Table interface: operation set(unsigned long, optional any)] expected: FAIL + [Memory interface: operation toFixedLengthBuffer()] + expected: FAIL + + [Memory interface: operation toResizableBuffer()] + expected: FAIL + + [Memory interface: [object WebAssembly.Memory\] must inherit property "toFixedLengthBuffer()" with the proper type] + expected: FAIL + + [Memory interface: [object WebAssembly.Memory\] must inherit property "toResizableBuffer()" with the proper type] + expected: FAIL + [idlharness.any.html] [Module interface: calling customSections(Module, USVString) on mod with too few arguments must throw TypeError] @@ -24,3 +36,15 @@ [Table interface: operation set(unsigned long, optional any)] expected: FAIL + + [Memory interface: operation toFixedLengthBuffer()] + expected: FAIL + + [Memory interface: operation toResizableBuffer()] + expected: FAIL + + [Memory interface: [object WebAssembly.Memory\] must inherit property "toFixedLengthBuffer()" with the proper type] + expected: FAIL + + [Memory interface: [object WebAssembly.Memory\] must inherit property "toResizableBuffer()" with the proper type] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/webidl/ecmascript-binding/legacy-factory-function-builtin-properties.window.js.ini b/tests/wpt/meta-legacy-layout/webidl/ecmascript-binding/legacy-factory-function-builtin-properties.window.js.ini new file mode 100644 index 00000000000..c1c0e6b9043 --- /dev/null +++ b/tests/wpt/meta-legacy-layout/webidl/ecmascript-binding/legacy-factory-function-builtin-properties.window.js.ini @@ -0,0 +1,3 @@ +[legacy-factory-function-builtin-properties.window.html] + [Legacy factory function property enumeration order of "length", "name", and "prototype"] + expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/webxr/idlharness.https.window.js.ini b/tests/wpt/meta-legacy-layout/webxr/idlharness.https.window.js.ini index f24aec48da2..a34d5829233 100644 --- a/tests/wpt/meta-legacy-layout/webxr/idlharness.https.window.js.ini +++ b/tests/wpt/meta-legacy-layout/webxr/idlharness.https.window.js.ini @@ -391,3 +391,6 @@ [XRSession interface: xrSession must inherit property "isSystemKeyboardSupported" with the proper type] expected: FAIL + + [XRInputSource interface: attribute skipRendering] + expected: FAIL diff --git a/tests/wpt/meta/MANIFEST.json b/tests/wpt/meta/MANIFEST.json index a706ebe4e35..e145c0f41df 100644 --- a/tests/wpt/meta/MANIFEST.json +++ b/tests/wpt/meta/MANIFEST.json @@ -614,6 +614,20 @@ {} ] ], + "chrome-336164421-crash.html": [ + "c45b69059bd82a95a182d6769b60f67bd0850d25", + [ + null, + {} + ] + ], + "chrome-336322507-crash.html": [ + "922c53b97741938b5c754c81e95a90a7735f9911", + [ + null, + {} + ] + ], "chrome-40286059-crash.html": [ "dbbeb5ac4df9bb2a17a757ece48a48cbbf3f677e", [ @@ -1789,6 +1803,13 @@ {} ] ], + "content-visibility-background-clip-crash.html": [ + "1fa645f45777fd82fd5f95dbdf8d83b53f1f0f14", + [ + null, + {} + ] + ], "content-visibility-continuations-crash.html": [ "e20d26fceedfc0071c61750776afa62c346d308e", [ @@ -4853,7 +4874,7 @@ ] ], "iframe-transition-destroyed-document-crash.html": [ - "31f6a10ed62c8c0689b7eb5b6b3b8574c0e0fa8e", + "13e743c3a1731bd20f3b8edabd8dbef86d418030", [ null, {} @@ -4881,7 +4902,7 @@ ] ], "root-element-display-none-during-transition-crash.html": [ - "b9c384d94a9a47dbb6bffa1c964239b799653216", + "d67bb256fdb346e21a41da181f7a737cab0c914c", [ null, {} @@ -8193,14 +8214,7 @@ {} ] ] - }, - "detached-context-crash.html": [ - "56271f12c3ae76bfd0d11e3abc8d09f15167c906", - [ - null, - {} - ] - ] + } }, "readable-streams": { "crashtests": { @@ -21845,56 +21859,56 @@ }, "events": { "drag-event-div-manual.html": [ - "79c0c4332d00cb704d0459168345d2eb5fc55d8c", + "505b0049bee124d72a489538779daadd6e941ed4", [ null, {} ] ], "drag-event-manual.html": [ - "d278b864bb9186de17be5ae3931384d1677e5f91", + "d3f517ea1d5140fd61e8f36fc134a22585e41953", [ null, {} ] ], "dragend-event-manual.html": [ - "8bfb1fb7b6eea55fff19542ad6d3e23faf6e0acc", + "b4bb621e888d5a632fe0ff35e95036d5634e41db", [ null, {} ] ], "dragenter-event-manual.html": [ - "e81b32949ccc855fe4d24f5c1b85ec78bee08ec4", + "23b404b0b177e937ae2f593f637e30a5598e45f6", [ null, {} ] ], "dragleave-event-manual.html": [ - "f6a405915fc2cd6ac04662b186d554c26c8b90e0", + "a400fa3417cbbc891516ba157c76c756778dec77", [ null, {} ] ], "dragover-event-manual.html": [ - "f8d99241d59f6cfa262ad135331f959e5f087d75", + "f37a33cff65cef2f4c57a9eb8df4b0f48d4fbac6", [ null, {} ] ], "dragstart-event-manual.html": [ - "20786648da33df11c99e335b7157ea50cf246e47", + "9128401ffaae34c58c90bb53b3307c3d748e28a1", [ null, {} ] ], "drop-event-manual.html": [ - "2897bd57135e69202d726acfe7bf6b7c2694b728", + "8393e386966620e0f724eb310731de59213b9f19", [ null, {} @@ -22300,14 +22314,14 @@ ] ], "effectAllowed-manual.html": [ - "08540b906a577785d64113b89b31133fc9d31bdd", + "61443da2c02105a14772942906fdac7a567d9607", [ null, {} ] ], "files-manual.html": [ - "7de0b4bbce11ba2cbfaa7e8e0021ebde3d9979b3", + "ffafb66db4f4bc3d9371f934f2246124c3437b35", [ null, {} @@ -22328,14 +22342,14 @@ ] ], "setData-manual.html": [ - "f0f7cae6009b5d6be3716f104d6e90a2a9d2e1bb", + "1438e932ad0b034c52a0616af838a62ca913f418", [ null, {} ] ], "types-manual.html": [ - "1730c4bc738897d8bd6f0e74febba949e25a932a", + "3aa1404e296ace9c20ff55f85098b1c6d522f968", [ null, {} @@ -32386,6 +32400,19 @@ {} ] ], + "page-box-000-print.html": [ + "aee317ab9752b33c91bc35d3cca5c57dd2212c9f", + [ + null, + [ + [ + "/css/css-page/page-box-000-print-ref.html", + "==" + ] + ], + {} + ] + ], "page-left-right-001-print.html": [ "044696fcca27ec7ebe18edab3053dfbb65af6bbc", [ @@ -121902,6 +121929,19 @@ {} ] ], + "position-visibility-anchors-visible-stacked-child.tentative.html": [ + "7c0d5dc6aadd36fc9a894dbf0add608f5c0e31c4", + [ + null, + [ + [ + "/css/css-anchor-position/position-visibility-anchors-visible-ref.html", + "==" + ] + ], + {} + ] + ], "position-visibility-anchors-visible-with-position.html": [ "43dd2cc782120763ee553c52b2f18a18308a1b7e", [ @@ -141004,7 +141044,7 @@ ] ], "ruby-002.html": [ - "d17cc563f38ce7505066724f2a2c9bc7f7c38a5c", + "2c4f6aae5ba72557af07f4dbcf5648673fdeea7e", [ null, [ @@ -143856,6 +143896,19 @@ {} ] ], + "scope-pseudo-element.html": [ + "29c446906092dc4bccec7fb4b21fb8b4a20b8f8f", + [ + null, + [ + [ + "/css/css-cascade/scope-pseudo-element-ref.html", + "==" + ] + ], + {} + ] + ], "scope-visited.html": [ "392aeb667b201072ff1a70aec745f75019cff54b", [ @@ -144994,6 +145047,19 @@ {} ] ], + "hsl-clamp-negative-saturation.html": [ + "25b54c2e2cb5a1b8debcfa150fbae1f1f65fa2fa", + [ + null, + [ + [ + "/css/css-color/hsl-clamp-negative-saturation-ref.html", + "==" + ] + ], + {} + ] + ], "hsla-001.html": [ "c3b2633a0a9af9814818f9e671c9f5c7db978524", [ @@ -145098,6 +145164,19 @@ {} ] ], + "hsla-clamp-negative-saturation.html": [ + "239151efe3a5cf2a3ffd88c533120e1eca3d2504", + [ + null, + [ + [ + "/css/css-color/hsla-clamp-negative-saturation-ref.html", + "==" + ] + ], + {} + ] + ], "hwb-001.html": [ "d1dce30c8ef85e0d5e536f261ad872185ac0e5f0", [ @@ -147155,19 +147234,6 @@ {} ] ], - "t424-hsl-clip-outside-gamut-b.xht": [ - "d66c2db925a8d5afdf0fc558c26b0d20bec92367", - [ - null, - [ - [ - "/css/css-color/t424-hsl-clip-outside-gamut-b-ref.html", - "==" - ] - ], - {} - ] - ], "t424-hsl-h-rotating-b.xht": [ "b310316f68a49e588863cccd96561173893fca6e", [ @@ -147402,19 +147468,6 @@ {} ] ], - "t425-hsla-clip-outside-device-gamut-b.xht": [ - "d30cf8c48393c1527d59a4bb87451f09a72491f6", - [ - null, - [ - [ - "/css/css-color/t425-hsla-clip-outside-device-gamut-b-ref.html", - "==" - ] - ], - {} - ] - ], "t425-hsla-h-rotating-b.xht": [ "a192442be7ddda6501238fe98f4f988c8f3be50a", [ @@ -147931,19 +147984,6 @@ ], {} ] - ], - "svg-as-image.html": [ - "6fc33f56ceafc180575abdcaf63c891104203fc7", - [ - null, - [ - [ - "/css/css-color-adjust/rendering/dark-color-scheme/svg-as-image-ref.html", - "==" - ] - ], - {} - ] ] } } @@ -150513,7 +150553,7 @@ ] ], "contain-layout-baseline-005.html": [ - "0971402e6b9f3afcf3224cd3e95d195606e0c698", + "fb706cbf0300eb09dd6daaf3a6e91417e40ad1b8", [ null, [ @@ -150551,8 +150591,8 @@ {} ] ], - "contain-layout-button-001.html": [ - "b53b28879e2ce884b61f741032afbf486afb3bf5", + "contain-layout-button-001.tentative.html": [ + "f5a664e566f6bc0324d14779f67b15e6d36d1615", [ null, [ @@ -150564,6 +150604,19 @@ {} ] ], + "contain-layout-button-002.tentative.html": [ + "15026d55f8c63712d19d8297c7c63e8543d3bf2f", + [ + null, + [ + [ + "/css/css-contain/reference/contain-layout-button-002-ref.html", + "==" + ] + ], + {} + ] + ], "contain-layout-cell-001.html": [ "66710060a7bf0bef0dd8e7a631761ef3e2e9e2cd", [ @@ -172317,6 +172370,19 @@ {} ] ], + "col-wrap-020.html": [ + "ffbfa711c31700f2825a58ebcaf83809f7254b0e", + [ + null, + [ + [ + "/css/reference/ref-filled-green-100px-square.xht", + "==" + ] + ], + {} + ] + ], "row-001.html": [ "d6f92804ac14ed9eac080ca103519dd44c788cb7", [ @@ -177341,6 +177407,19 @@ {} ] ], + "font-unicode-PUA-primary-font.html": [ + "1b03c98e403a2bd7e8a109f3b70d9c29e5f6d98f", + [ + null, + [ + [ + "/css/css-fonts/matching/font-unicode-PUA-primary-font-notref.html", + "!=" + ] + ], + {} + ] + ], "font-unicode-PUA.html": [ "9ed897aa006317c2eee63ecd69cc37c068c34217", [ @@ -178154,6 +178233,19 @@ {} ] ], + "variation-sequences.html": [ + "91e46a84d71cc873e006bd59c328d54c8c614b52", + [ + null, + [ + [ + "/css/css-fonts/variation-sequences-ref.html", + "==" + ] + ], + {} + ] + ], "variations": { "font-descriptor-range-reversed.html": [ "14d426245d3bdb56e67007edde5e9a49473e5694", @@ -185955,7 +186047,7 @@ }, "track-sizing": { "masonry-track-sizing-check-grid-height-on-resize.html": [ - "06c2901f27bf8c8edabf434cf79bf7915e5dd09e", + "dbff19f28e1b00a18c7a34678b444a43e162d0c8", [ null, [ @@ -195119,7 +195211,7 @@ }, "text-box-trim": { "text-box-trim-half-leading-block-box-001.html": [ - "2cbf1c26dbfa97b92a8e1a2739dae24b2042d4e3", + "14429b10022448c155c3723f9798e3e66d4022bc", [ null, [ @@ -195130,6 +195222,19 @@ ], {} ] + ], + "text-box-trim-half-leading-block-box-002.html": [ + "fdf5b71d4345f8254d8542b9f66d1f43d1b5710f", + [ + null, + [ + [ + "/css/css-inline/text-box-trim/text-box-trim-half-leading-block-box-002-ref.html", + "==" + ] + ], + {} + ] ] } }, @@ -199011,7 +199116,7 @@ ] ], "clip-path-animation-fixed-position-rounding-error.html": [ - "8a02a5b2a6d6635c2b13633ae62e296704c962a6", + "02a14ad65fec0b62bf4e01726cc4ef8326b94702", [ null, [ @@ -199020,11 +199125,27 @@ "==" ] ], - {} + { + "fuzzy": [ + [ + null, + [ + [ + 0, + 64 + ], + [ + 0, + 400 + ] + ] + ] + ] + } ] ], "clip-path-animation-fixed-position.html": [ - "3ffc2a30a7f3b31ba7c5a8f5f8c0f3d727ffd9f3", + "20fc8e52d89cdef2a5ff742314087542b0c4f3f7", [ null, [ @@ -199040,11 +199161,11 @@ [ [ 0, - 10 + 60 ], [ 0, - 150 + 350 ] ] ] @@ -199683,6 +199804,19 @@ ], {} ] + ], + "two-clip-path-animation-diff-length4.html": [ + "0a893cb75637e4d1b63e85cca77deff844c37c11", + [ + null, + [ + [ + "/css/css-masking/clip-path/animations/two-clip-path-animation-diff-length1-ref.html", + "==" + ] + ], + {} + ] ] }, "clip-path-blending-offset.html": [ @@ -217426,6 +217560,45 @@ {} ] ], + "position-sticky-left-004.html": [ + "79188e84b0da0d25ded2630282bc62f72cc4a808", + [ + null, + [ + [ + "/css/reference/ref-filled-green-100px-square.xht", + "==" + ] + ], + {} + ] + ], + "position-sticky-left-005.html": [ + "b029e593635152f36878b53e9462c479351eedeb", + [ + null, + [ + [ + "/css/reference/ref-filled-green-100px-square.xht", + "==" + ] + ], + {} + ] + ], + "position-sticky-left-006.html": [ + "63fa1ebfd89c6a164d524b0af8f406ea1ee7b938", + [ + null, + [ + [ + "/css/reference/ref-filled-green-100px-square.xht", + "==" + ] + ], + {} + ] + ], "position-sticky-nested-inline.html": [ "92eda147bd4e54cc70081eb36eb8bb9ae33f19c2", [ @@ -217569,6 +217742,19 @@ {} ] ], + "position-sticky-scrolled-remove-sibling-002.html": [ + "870efa417c745f714f55805af8b210bb72a502f9", + [ + null, + [ + [ + "/css/reference/ref-filled-green-100px-square.xht", + "==" + ] + ], + {} + ] + ], "position-sticky-stacking-context-002.html": [ "1cc162880a37559fd2879912b4418cbba9cbe7cb", [ @@ -217790,6 +217976,45 @@ {} ] ], + "position-sticky-top-004.html": [ + "bf0b87ab36d857eb0401d610806ed9924f2a4726", + [ + null, + [ + [ + "/css/reference/ref-filled-green-100px-square.xht", + "==" + ] + ], + {} + ] + ], + "position-sticky-top-005.html": [ + "5f76a722b52b0b725930895ffaa14b35d35ba843", + [ + null, + [ + [ + "/css/reference/ref-filled-green-100px-square.xht", + "==" + ] + ], + {} + ] + ], + "position-sticky-top-006.html": [ + "4ac5310bf35625679ab431bae488c5205b6d04b7", + [ + null, + [ + [ + "/css/reference/ref-filled-green-100px-square.xht", + "==" + ] + ], + {} + ] + ], "position-sticky-top-and-bottom-003.html": [ "af68bcef729aca98fe7c792df3deb067fbf32763", [ @@ -218149,58 +218374,6 @@ {} ] ], - "cascade-highlight-001.html": [ - "6068a076f0da862b834982b60fc1d46c94c72b46", - [ - null, - [ - [ - "/css/css-pseudo/cascade-highlight-001-ref.html", - "==" - ] - ], - {} - ] - ], - "cascade-highlight-002.html": [ - "50be805f011c223caf99fc55d0052161428eeb3f", - [ - null, - [ - [ - "/css/css-pseudo/cascade-highlight-001-ref.html", - "==" - ] - ], - {} - ] - ], - "cascade-highlight-004.html": [ - "56abba71569c05dfbc3244c330414b53d1866c5a", - [ - null, - [ - [ - "/css/css-pseudo/cascade-highlight-004-ref.html", - "==" - ] - ], - {} - ] - ], - "cascade-highlight-005.html": [ - "2c6ba60270ca78659647c98ba94aca95ff6de541", - [ - null, - [ - [ - "/css/css-pseudo/reference/cascade-highlight-005-ref.html", - "==" - ] - ], - {} - ] - ], "file-selector-button-001.html": [ "48b2852de820f61ce5344584ab1ff3aa2313adcf", [ @@ -218942,243 +219115,375 @@ {} ] ], - "highlight-cascade-001.html": [ - "f237e9eca7321123c6d9de5f40cd95a5cab62fc9", - [ - null, + "highlight-cascade": { + "cascade-highlight-001.html": [ + "f441adf3b90058d9691a7bbb53d1bf7b38a783ba", [ + null, [ - "/css/css-pseudo/highlight-cascade-001-ref.html", - "==" - ] - ], - {} - ] - ], - "highlight-cascade-002.html": [ - "c01d3c796e5ec88c16dae4c1819908a03cba4078", - [ - null, + [ + "/css/css-pseudo/highlight-cascade/cascade-highlight-001-ref.html", + "==" + ] + ], + {} + ] + ], + "cascade-highlight-002.html": [ + "d10bdb9b042f86eef15d06ffafc7e5f775bf0569", [ + null, [ - "/css/css-pseudo/highlight-cascade-002-ref.html", - "==" - ] - ], - {} - ] - ], - "highlight-cascade-003.html": [ - "61bc46d4c50feab4f275e6d0785bf729c4239b27", - [ - null, + [ + "/css/css-pseudo/highlight-cascade/cascade-highlight-001-ref.html", + "==" + ] + ], + {} + ] + ], + "cascade-highlight-004.html": [ + "295321a1720b9af445b1a294721f79b7ca7928e7", [ + null, [ - "/css/css-pseudo/highlight-cascade-003-ref.html", - "==" - ] - ], - {} - ] - ], - "highlight-cascade-004.html": [ - "eb181096ce1ee035aa10e21e6f35fba58c5c7acb", - [ - null, + [ + "/css/css-pseudo/highlight-cascade/cascade-highlight-004-ref.html", + "==" + ] + ], + {} + ] + ], + "cascade-highlight-005.html": [ + "25ad85fe70788088d5fbd0a0ba87b26e629a2d08", [ + null, [ - "/css/css-pseudo/highlight-cascade-004-ref.html", - "==" - ] - ], - {} - ] - ], - "highlight-cascade-005.html": [ - "008f56aded9f2b7be43e346189f5a7b552065064", - [ - null, + [ + "/css/css-pseudo/highlight-cascade/cascade-highlight-005-ref.html", + "==" + ] + ], + {} + ] + ], + "highlight-cascade-001.html": [ + "18b3635b3f7ac6c347f8c91fc57acaadbbf50dfc", [ + null, [ - "/css/css-pseudo/highlight-cascade-005-ref.html", - "==" - ] - ], - {} - ] - ], - "highlight-cascade-006.xhtml": [ - "fb6d07f8f0b6b420a3fcf71480055756b09a1901", - [ - null, + [ + "/css/css-pseudo/highlight-cascade/highlight-cascade-001-ref.html", + "==" + ] + ], + {} + ] + ], + "highlight-cascade-003.html": [ + "b29f37528c0b649ed9df2f9ad162b9262197e32b", [ + null, [ - "/css/css-pseudo/highlight-cascade-006-ref.xhtml", - "==" - ] - ], - {} - ] - ], - "highlight-cascade-008.html": [ - "10ca924b8fd1d24089011c71584071a6e84423e4", - [ - null, + [ + "/css/css-pseudo/highlight-cascade/highlight-cascade-003-ref.html", + "==" + ] + ], + {} + ] + ], + "highlight-cascade-004.html": [ + "f3155bcec6b3ecb9f15013e031ae2506940633a4", [ + null, [ - "/css/css-pseudo/highlight-cascade-008-ref.html", - "==" - ] - ], - { - "fuzzy": [ [ - null, + "/css/css-pseudo/highlight-cascade/highlight-cascade-004-ref.html", + "==" + ] + ], + {} + ] + ], + "highlight-cascade-005.html": [ + "958bdf65447e5399460a621dc7b328915596102f", + [ + null, + [ + [ + "/css/css-pseudo/highlight-cascade/highlight-cascade-005-ref.html", + "==" + ] + ], + {} + ] + ], + "highlight-cascade-006.xhtml": [ + "4a37af7c255356c788c1c4e32be47fffa1f2ad98", + [ + null, + [ + [ + "/css/css-pseudo/highlight-cascade/highlight-cascade-006-ref.xhtml", + "==" + ] + ], + {} + ] + ], + "highlight-cascade-008.html": [ + "720e2f0469d056475e0d502a19546e6fbfb4567c", + [ + null, + [ + [ + "/css/css-pseudo/highlight-cascade/highlight-cascade-008-ref.html", + "==" + ] + ], + { + "fuzzy": [ [ + null, [ - 0, - 255 - ], - [ - 0, - 10 + [ + 0, + 255 + ], + [ + 0, + 10 + ] ] ] ] - ] - } - ] - ], - "highlight-currentcolor-painting-properties-001.html": [ - "76d80d228a0482acfd8d1c57591eca540b445e6f", - [ - null, + } + ] + ], + "highlight-currentcolor-painting-properties-001.html": [ + "efdb9016bc213cd4502557fd4f8eb6b77487c172", [ + null, [ - "/css/css-pseudo/highlight-currentcolor-painting-properties-001-ref.html", - "==" - ] - ], - { - "fuzzy": [ [ - null, + "/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-001-ref.html", + "==" + ] + ], + { + "fuzzy": [ [ + null, [ - 0, - 50 - ], - [ - 0, - 150 + [ + 0, + 50 + ], + [ + 0, + 150 + ] ] ] ] - ] - } - ] - ], - "highlight-currentcolor-painting-properties-002.html": [ - "ac3677c1cff258088942b00dd9bc0f903df5cb35", - [ - null, + } + ] + ], + "highlight-currentcolor-painting-properties-002.html": [ + "d02dc9e8c5ba3de05bdc6178944808f97f1a0264", [ + null, [ - "/css/css-pseudo/highlight-currentcolor-painting-properties-002-ref.html", - "==" - ] - ], - { - "fuzzy": [ [ - null, + "/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-002-ref.html", + "==" + ] + ], + { + "fuzzy": [ [ + null, [ - 0, - 50 - ], - [ - 0, - 150 + [ + 0, + 50 + ], + [ + 0, + 150 + ] ] ] ] - ] - } - ] - ], - "highlight-currentcolor-painting-text-shadow-001.html": [ - "141556f935b8e38f0352fd0a0c32212f2c62debf", - [ - null, + } + ] + ], + "highlight-currentcolor-painting-text-shadow-001.html": [ + "c82da8c380ed2f8f13909e001a8f4e170983dbe5", [ + null, [ - "/css/css-pseudo/highlight-currentcolor-painting-text-shadow-001-ref.html", - "==" - ] - ], - {} - ] - ], - "highlight-currentcolor-painting-text-shadow-002.html": [ - "77858729afaf8c55b435094baceb5959b67d3d46", - [ - null, + [ + "/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-001-ref.html", + "==" + ] + ], + {} + ] + ], + "highlight-currentcolor-painting-text-shadow-002.html": [ + "870243f5013b4a90f72787ee01c1631f374def1a", [ + null, [ - "/css/css-pseudo/highlight-currentcolor-painting-text-shadow-002-ref.html", - "==" - ] - ], - {} - ] - ], - "highlight-currentcolor-root-explicit-default-001.html": [ - "a1512f014df4a38b81ac4c60cb1e2383542a60bd", - [ - null, + [ + "/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-002-ref.html", + "==" + ] + ], + {} + ] + ], + "highlight-currentcolor-root-explicit-default-001.html": [ + "1869f8ab53fbc1434497ddd49a3b5a0d04b9e486", [ + null, [ - "/css/css-pseudo/highlight-currentcolor-root-explicit-default-001-ref.html", - "==" - ] - ], - {} - ] - ], - "highlight-currentcolor-root-explicit-default-002.html": [ - "fc5698faa1e794bde4ca69cf6b83a5f9868d1171", - [ - null, + [ + "/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-001-ref.html", + "==" + ] + ], + {} + ] + ], + "highlight-currentcolor-root-explicit-default-002.html": [ + "fe6d80be2e93ec9991209b19743655bbb0beb82f", [ + null, [ - "/css/css-pseudo/highlight-currentcolor-root-explicit-default-002-ref.html", - "==" - ] - ], - {} - ] - ], - "highlight-currentcolor-root-implicit-default-001.html": [ - "ecf787b9b41a3c684ea2d1175aafa9049c097f84", - [ - null, + [ + "/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-002-ref.html", + "==" + ] + ], + {} + ] + ], + "highlight-currentcolor-root-implicit-default-001.html": [ + "ecf787b9b41a3c684ea2d1175aafa9049c097f84", [ + null, [ - "/css/css-pseudo/highlight-currentcolor-root-implicit-default-ref.html", - "==" - ] - ], - {} + [ + "/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-implicit-default-ref.html", + "==" + ] + ], + {} + ] + ], + "highlight-currentcolor-root-implicit-default-002.html": [ + "420cc5ba15e2f83e475db36f9574e2401a5c1217", + [ + null, + [ + [ + "/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-implicit-default-ref.html", + "==" + ] + ], + {} + ] + ], + "highlight-paired-cascade-001.html": [ + "7f370238dfa450d6d0f3f9fe9ed24202bb06fcf6", + [ + null, + [ + [ + "/css/css-pseudo/highlight-cascade/highlight-paired-cascade-001-ref.html", + "==" + ] + ], + {} + ] + ], + "highlight-paired-cascade-002.html": [ + "626fc57558d1eae8593e58e1ace2fbda7b47ed59", + [ + null, + [ + [ + "/css/css-pseudo/highlight-cascade/highlight-paired-cascade-002-ref.html", + "==" + ] + ], + {} + ] + ], + "highlight-paired-cascade-003.html": [ + "8c621cc777c187902f286e977568f0aced8dde52", + [ + null, + [ + [ + "/css/css-pseudo/highlight-cascade/highlight-paired-cascade-003-ref.html", + "==" + ] + ], + {} + ] + ], + "highlight-paired-cascade-004.html": [ + "0a73d006b559fc16cde5b09a1d8af9d2c1248088", + [ + null, + [ + [ + "/css/css-pseudo/highlight-cascade/highlight-paired-cascade-004-notref.html", + "!=" + ] + ], + {} + ] + ], + "highlight-paired-cascade-005.html": [ + "4ee6db3a091536c6b2fbc15314728743c01230bf", + [ + null, + [ + [ + "/css/css-pseudo/highlight-cascade/highlight-paired-cascade-005-ref.html", + "==" + ] + ], + {} + ] + ], + "highlight-paired-cascade-006.html": [ + "bef3601ccbf833890a3284cd25e3232b9fe72517", + [ + null, + [ + [ + "/css/css-pseudo/highlight-cascade/highlight-paired-cascade-006-ref.html", + "==" + ] + ], + {} + ] ] - ], - "highlight-currentcolor-root-implicit-default-002.html": [ - "420cc5ba15e2f83e475db36f9574e2401a5c1217", + }, + "highlight-custom-properties-dynamic-001.html": [ + "836ef546fa60509b98e43429ec42cb05293fb562", [ null, [ [ - "/css/css-pseudo/highlight-currentcolor-root-implicit-default-ref.html", + "/css/css-pseudo/highlight-custom-properties-dynamic-001-ref.html", "==" ] ], @@ -219290,91 +219595,71 @@ {} ] ], - "highlight-painting-soft-hyphens-001.html": [ - "17b5f6a37c4c2489959f41bed4ca53c352a20a4d", + "highlight-painting-shadows-horizontal.html": [ + "b1762e85ae31fc6dc640fd038ec0edf7ee7d4e25", [ null, [ [ - "/css/css-pseudo/highlight-painting-soft-hyphens-001-ref.html", + "/css/css-pseudo/highlight-painting-shadows-horizontal-ref.html", "==" ] ], - {} - ] - ], - "highlight-paired-cascade-001.html": [ - "09e5abf9a3285ff219044a84b61bbc239f405689", - [ - null, - [ - [ - "/css/css-pseudo/highlight-paired-cascade-001-ref.html", - "==" - ] - ], - {} - ] - ], - "highlight-paired-cascade-002.html": [ - "affbe9562975c351f848bdc927c1cdcc56e118f5", - [ - null, - [ - [ - "/css/css-pseudo/highlight-paired-cascade-002-ref.html", - "==" + { + "fuzzy": [ + [ + null, + [ + [ + 0, + 32 + ], + [ + 0, + 20 + ] + ] + ] ] - ], - {} + } ] ], - "highlight-paired-cascade-003.html": [ - "250e320210c65f6b8538adf849362d5e34da634a", + "highlight-painting-shadows-vertical.html": [ + "7187c34da4495fbaded9b9986e5a26889fde6beb", [ null, [ [ - "/css/css-pseudo/highlight-paired-cascade-003-ref.html", + "/css/css-pseudo/highlight-painting-shadows-vertical-ref.html", "==" ] ], - {} - ] - ], - "highlight-paired-cascade-004.html": [ - "61e2b7d7f0735e75a34b043354a60ac58659083b", - [ - null, - [ - [ - "/css/css-pseudo/highlight-paired-cascade-004-notref.html", - "!=" - ] - ], - {} - ] - ], - "highlight-paired-cascade-005.html": [ - "50677c811e78fc81496d462e00fc4977e5d29ff9", - [ - null, - [ - [ - "/css/css-pseudo/highlight-paired-cascade-005-ref.html", - "==" + { + "fuzzy": [ + [ + null, + [ + [ + 0, + 32 + ], + [ + 0, + 4 + ] + ] + ] ] - ], - {} + } ] ], - "highlight-paired-cascade-006.html": [ - "20c03282c81a03df93d912080ede2bb5fc8e64ed", + "highlight-painting-soft-hyphens-001.html": [ + "17b5f6a37c4c2489959f41bed4ca53c352a20a4d", [ null, [ [ - "/css/css-pseudo/highlight-paired-cascade-006-ref.html", + "/css/css-pseudo/highlight-painting-soft-hyphens-001-ref.html", "==" ] ], @@ -219382,7 +219667,7 @@ ] ], "highlight-styling-001.html": [ - "63d8ee1eda47dec0eeaf30f56316e4203e49f182", + "7fe76d19380caff7ed1efe3e5023d629dcb0f96a", [ null, [ @@ -219395,7 +219680,7 @@ ] ], "highlight-styling-002.html": [ - "2f7cc29128e4dc8de7922922f33cab8451c0eaf1", + "351eacac8852f8bd6d982e30f07d8f3f269e85cc", [ null, [ @@ -220875,7 +221160,7 @@ ] ], "target-text-dynamic-004.html": [ - "279ec674b402e6b2bc47aea07487dda8ca10f0dd", + "35f2542c766735f3de82f4e43492b1874c60437d", [ null, [ @@ -220887,6 +221172,32 @@ {} ] ], + "target-text-shadow-horizontal.html": [ + "1ded1360ff047d2432f59fc0a6549a8759af0d24", + [ + null, + [ + [ + "/css/css-pseudo/target-text-shadow-horizontal-ref.html", + "==" + ] + ], + {} + ] + ], + "target-text-shadow-vertical.html": [ + "088f6fc175e7a581b74709c7214e22401098cd52", + [ + null, + [ + [ + "/css/css-pseudo/target-text-shadow-vertical-ref.html", + "==" + ] + ], + {} + ] + ], "target-text-text-decoration-001.html": [ "d90d5d96630d05ab88430b88fe4a4ad8985a6fac", [ @@ -223774,6 +224085,19 @@ {} ] ], + "part-after-combinator-invalidation.html": [ + "76beabe8855fad7344690e1388fb2251abf23033", + [ + null, + [ + [ + "/css/css-shadow-parts/part-after-combinator-invalidation-ref.html", + "==" + ] + ], + {} + ] + ], "part-nested-pseudo.html": [ "f1753de7752840cd2841cb8002c10ac08038cf18", [ @@ -281172,6 +281496,19 @@ {} ] ], + "calc-rounding-003.html": [ + "bf609e1c1d4bfcb597bd76d9eb1e4b5c501985fb", + [ + null, + [ + [ + "/css/css-values/calc-rounding-003-ref.html", + "==" + ] + ], + {} + ] + ], "calc-text-indent-1.html": [ "f1480f46e773e8767367306dd16a64950f176880", [ @@ -287096,6 +287433,19 @@ {} ] ], + "pseudo-element-overflow-hidden.html": [ + "e40df4f6a8b196d52eb8b4da571e987af75ebfe3", + [ + null, + [ + [ + "/css/css-view-transitions/pseudo-element-overflow-hidden-ref.html", + "==" + ] + ], + {} + ] + ], "pseudo-element-preserve-3d.html": [ "474f743e1c50ac0dacf15980cc75a08e5bf9303f", [ @@ -307472,6 +307822,19 @@ {} ] ], + "prefers-color-scheme-svg-as-image.html": [ + "65d6556f7af321d40854288bb3ad546d0e5a2714", + [ + null, + [ + [ + "/css/mediaqueries/prefers-color-scheme-svg-as-image-ref.html", + "==" + ] + ], + {} + ] + ], "prefers-color-scheme-svg-image-normal-with-meta-dark.html": [ "be633826ece7b29ff698d67bc6f919cbea29f94c", [ @@ -327553,7 +327916,7 @@ ] ], "bounded-sizes-reftest.tentative.html": [ - "3d24d30679e77872be2ac918ea7dd60c7d84446c", + "45ffb633c3dc4aab7b40e750e922abc7a27e6275", [ null, [ @@ -342199,7 +342562,7 @@ [] ], "interfaces.yml": [ - "04636e0bb3d5f923321f7979b829c01785465017", + "ff2a679b5983ed7913d8eaafff605b31bde38faf", [] ], "manifest.yml": [ @@ -342207,7 +342570,7 @@ [] ], "regen_certs.yml": [ - "d3e1240e79aadfeeb47a728b3c38df4d8fa9b50d", + "634ac202d5f18815d4bce4a2f95203558ab14ce7", [] ] } @@ -346135,7 +346498,13 @@ }, "resources": { "helpers.js": [ - "dd9e191c4dbcbe4ee53034310639bc76a9307285", + "ad80c28847f3a6802a56527e11d89a2829ecc8f9", + [] + ] + }, + "user-activation": { + "README.md": [ + "b9aa9a212342470c1e4c4e5b15b38197600297e7", [] ] } @@ -367378,7 +367747,7 @@ [] ], "accounts_check_same_site_strict.py": [ - "a6f385feac1ccf0885b44c43a0d0e737e4c308f6", + "796ac003cbc0bb34773b50c1c5de4351aae8dddd", [] ], "accounts_no_approved_clients.py": [ @@ -367538,7 +367907,7 @@ [] ], "token_check_same_site_strict.py": [ - "8a4b3a234bdb99f38354f706d740b823ccc6e785", + "4e55bf27f610032b30eb3278ddd0f7db4716efef", [] ], "token_with_account_id.py": [ @@ -367571,7 +367940,7 @@ ] }, "fedcm-helper.sub.js": [ - "f0031fa531afff7bd90a95f37561b7955b6c3a7f", + "308950e1e29c7cb0a10d366fb40f673e6befc28f", [] ], "fedcm-iframe-level2.html": [ @@ -367635,7 +368004,7 @@ [] ], "set_cookie.headers": [ - "ddeb0bb3d6d81088f63178a2d8ffb7de001b736b", + "df223115a7fa546e682275e8bb9d9af624ea333a", [] ] } @@ -388426,6 +388795,10 @@ "661016619c0c46c113dedc57aa890f62b35e137c", [] ], + "scope-pseudo-element-ref.html": [ + "2ad6a0995a9e3bb9db0131eb2c42b19d09768fb4", + [] + ], "scope-visited-ref.html": [ "91efd659216ab1de4efdfe2cac961bc6200ae62b", [] @@ -388616,6 +388989,14 @@ "dc7e324e0edbd096f5ac6a31346836377e366c8d", [] ], + "hsl-clamp-negative-saturation-ref.html": [ + "33a82517ae7f68bc45e59239cb1c7a1cc1dfb09a", + [] + ], + "hsla-clamp-negative-saturation-ref.html": [ + "bbf5c3b037d071467c1286941b3edf5a79bf6ce7", + [] + ], "htaccess": [ "26cbb024e8aedc9b8f613872b76dd1d9f4dedc31", [] @@ -388914,10 +389295,6 @@ "f79bc4beb3cac388bdc8c10d0169d1b41f8723f9", [] ], - "t424-hsl-clip-outside-gamut-b-ref.html": [ - "7d1f1bf2786074794f9069ea14080cd2d6731ded", - [] - ], "t424-hsl-h-rotating-b-ref.html": [ "60585ea91ae15a1e04b48af9ebb008d1926fbb17", [] @@ -388990,10 +389367,6 @@ "bc21fe36a26a573b5922aa84b9934e003bcadb49", [] ], - "t425-hsla-clip-outside-device-gamut-b-ref.html": [ - "d35a608fb58c1f9258477207ddb12dd89e93f9a1", - [] - ], "t425-hsla-h-rotating-b-ref.html": [ "2aec2f33e18d9599530704bbfea224a6af718243", [] @@ -389062,6 +389435,10 @@ ], "rendering": { "dark-color-scheme": { + "WEB_FEATURES.yml": [ + "48bbe25901e085455ca7569c4bfbc0147fd40455", + [] + ], "color-scheme-change-checkbox-notref.html": [ "921482afcdf10cc38d113d0d428e1df43d09dd66", [] @@ -389114,16 +389491,8 @@ "prefers-color-scheme-blue-purple.html": [ "99d687b47f4685e8ca0960fbcfd7aaa7bdafc19a", [] - ], - "prefers-color-scheme.svg": [ - "3afcac70fe9b561280f32d53420404a3cf23e1cf", - [] ] - }, - "svg-as-image-ref.html": [ - "1ff9d88f302f90dbe4b708657e9fb0bc1378453a", - [] - ] + } } } }, @@ -389820,7 +390189,7 @@ [] ], "contain-layout-baseline-005-ref.html": [ - "360652c9397ee8b69a4bf07c763148eafcc64be0", + "83340137ac6ac90bcab35f1e979bfae5b7ca7465", [] ], "contain-layout-breaks-002-ref.html": [ @@ -389828,7 +390197,11 @@ [] ], "contain-layout-button-001-ref.html": [ - "da83204dce641b37a820cf47d2201d94a0fd60cb", + "c20efbbb39d4cc9181db3a332d4846c214695c65", + [] + ], + "contain-layout-button-002-ref.html": [ + "85b98ee4a66b1a9d66f0910a2e436c1136f7ce86", [] ], "contain-layout-cell-001-ref.html": [ @@ -394857,6 +395230,10 @@ "ebbef02f628baa63b2cb565f0f2850899d54ae65", [] ], + "font-unicode-PUA-primary-font-notref.html": [ + "df9b829a41905dc8c393c3b8f64394475e8d9e23", + [] + ], "font-unicode-PUA-ref.html": [ "01b95e8e72e59bf248148555d32a5e06c943b166", [] @@ -395000,7 +395377,37 @@ "separator-test-font.ttf": [ "26cc99783d079f91ae9c1627a6af0c56d72d324a", [] - ] + ], + "vs": { + "MPLUS1-Regular_without-cmap14-subset.ttf": [ + "b77096690df1b4136cb7907708c778d716433c59", + [] + ], + "NotoColorEmoji-Regular_subset.ttf": [ + "24ab79fd05295151de61a16be460b4f8e7892f7c", + [] + ], + "NotoEmoji-Regular_subset.ttf": [ + "0b054c7c8d597e6d600817ec05543d7612e79351", + [] + ], + "NotoEmoji-Regular_without-cmap14-subset.ttf": [ + "3d00ef81dea38d7de7b95538a1217126b897f8c9", + [] + ], + "NotoSansJP-Regular_with-cmap14-subset.ttf": [ + "edcb98e37458b23ba6bf4ebdff01b19e0ad8b6c3", + [] + ], + "NotoSansMath-Regular_without-cmap14-subset.ttf": [ + "5436d06dae52733c018bb01371dfca0bb2bc6afd", + [] + ], + "STIXTwoMath-Regular_with-cmap14-subset.ttf": [ + "c9720cf9ce712da3631a327e1ee00d399e28613b", + [] + ] + } }, "rex-in-monospace-ref.html": [ "27972869e6cf3b8c5cd57670b9b7c3533353a01e", @@ -395199,6 +395606,10 @@ "font-variant-features.css": [ "fbe701918dc2739cd381d2c32f310bdc3f56848d", [] + ], + "variation-sequences.css": [ + "5977f17b678c286556b45983ed8212a6cce33e9b", + [] ] }, "diamond.png": [ @@ -401095,6 +401506,10 @@ "font-variant-features.js": [ "4b56fee193956710b847ba79c5f9c3a5a7d15a33", [] + ], + "variation-sequences.js": [ + "84c5a1a9c77932b9b4aedc2be24a38e148bae00d", + [] ] }, "margin-collapse-2em-space.png": [ @@ -401246,6 +401661,10 @@ "a0721426016c949369ae0114211136f45ce51b27", [] ], + "variation-sequences-ref.html": [ + "a44f18bb3e1f3584aa9b3407a0b7033dddd234aa", + [] + ], "variations": { "META.yml": [ "53487a6d85ecaafdc33738292312120ffed4bb6a", @@ -402624,7 +403043,7 @@ }, "track-sizing": { "masonry-track-sizing-check-grid-height-on-resize-ref.html": [ - "71968864924cafa3166a352d15e4d0d1b81383d1", + "53447e0dad88454672e0a70fbdbaaa6abc73055e", [] ], "masonry-track-sizing-explicit-block-ref.html": [ @@ -404552,6 +404971,10 @@ "text-box-trim-half-leading-block-box-001-ref.html": [ "2dfe344ce35f59888337a34f1ae864925f9a35e4", [] + ], + "text-box-trim-half-leading-block-box-002-ref.html": [ + "8c10a8037064a09d8a0c04357315ad8498e3a31d", + [] ] } }, @@ -405225,7 +405648,7 @@ [] ], "clip-path-animation-fixed-position-rounding-error-ref.html": [ - "a069e4d3ae5bc82ea9547b01e7958b8f7eead645", + "e9f54f3ad3dd757070d63ecdc3c3ae4123d213e8", [] ], "clip-path-animation-font-size-ref.html": [ @@ -407856,6 +408279,10 @@ "d8468e3b14dc0346f178727336572f5122b0e4bf", [] ], + "page-box-000-print-ref.html": [ + "d39bd738507490b5590afa7a4eb6cce9766b63bc", + [] + ], "page-left-right-001-print-ref.html": [ "1eb011bed8a0dde4a65494d54fc2e2bd9a042080", [] @@ -409000,14 +409427,6 @@ "c15156138e6a7e2e42af3621362911cbd69dda24", [] ], - "cascade-highlight-001-ref.html": [ - "25dbeadb2ee2dbf6d7f568eb6e15443715e77360", - [] - ], - "cascade-highlight-004-ref.html": [ - "e755283a32dc1332c29d61342edd6802d2be52fc", - [] - ], "file-selector-button-001-notref.html": [ "e137ba05103d71203dc09083fa71fbbed1b21617", [] @@ -409172,60 +409591,98 @@ "ecdb308f2f426af4f2763fabb30f7244f42fcf62", [] ], - "highlight-cascade-001-ref.html": [ - "a18690962fcd4e73d5832f76e7afb8ee681ae24e", - [] - ], - "highlight-cascade-002-ref.html": [ - "17629a6dbad7fb5700b35b8bd0ea5280dba69140", - [] - ], - "highlight-cascade-003-ref.html": [ - "256c28ba0c068630cf675698e019b656a6c56da7", - [] - ], - "highlight-cascade-004-ref.html": [ - "f9bf83f9c1d63b50b61b96a4edadc1241d3c640b", - [] - ], - "highlight-cascade-005-ref.html": [ - "20d2b0bdb3a1dc84e9d285e0b1d350dde45320f3", - [] - ], - "highlight-cascade-006-ref.xhtml": [ - "df0a56123e033dd4536bc0425353c82bd2d0156b", - [] - ], - "highlight-cascade-008-ref.html": [ - "c87ddcf93c9ae7f0a63fe305e5155e3549fc9ee7", - [] - ], - "highlight-currentcolor-painting-properties-001-ref.html": [ - "93e2002f4aa2bde2c0d1adbb13e0dfd2abf82421", - [] - ], - "highlight-currentcolor-painting-properties-002-ref.html": [ - "11ca501065e5567345ef6ac0541515d45ff20184", - [] - ], - "highlight-currentcolor-painting-text-shadow-001-ref.html": [ - "d6ee33cda749a55a89b28c5f253d2d956d8081e8", - [] - ], - "highlight-currentcolor-painting-text-shadow-002-ref.html": [ - "fb2696a55a1da63de8bc7962ddf7d20a4f31a861", - [] - ], - "highlight-currentcolor-root-explicit-default-001-ref.html": [ - "794796a88f6a7b1e19c2cf9f8336838722b998b4", - [] - ], - "highlight-currentcolor-root-explicit-default-002-ref.html": [ - "31759483174842e7dcc6d0fcb1cea8883f593c06", - [] - ], - "highlight-currentcolor-root-implicit-default-ref.html": [ - "67ecb8df640f72e1725e42a648f2c6e1f33490e2", + "highlight-cascade": { + "cascade-highlight-001-ref.html": [ + "2a2907b10a4ab04072ce5e03e2e50e4cdacc371d", + [] + ], + "cascade-highlight-004-ref.html": [ + "9b8507c4cc89dda1e80604e6e97fe9eaef814200", + [] + ], + "cascade-highlight-005-ref.html": [ + "d1e597e05b8434bbb9b769cc62841662df0481b1", + [] + ], + "highlight-cascade-001-ref.html": [ + "b96ffce5ab3245807c7328454dee48cdd289ee2a", + [] + ], + "highlight-cascade-003-ref.html": [ + "68964149baf0d6f1f173b7497d3c6623e125f5a4", + [] + ], + "highlight-cascade-004-ref.html": [ + "5845e936209920ccfe2af2e0cd0fe9bb2b1fa348", + [] + ], + "highlight-cascade-005-ref.html": [ + "f0ea3faa764f9aa05a71e33950402082cd44afe1", + [] + ], + "highlight-cascade-006-ref.xhtml": [ + "643dd793254ab9602d2d12189baf562a17de7aa8", + [] + ], + "highlight-cascade-008-ref.html": [ + "70b9d958b5fcb61973cde1955c9118348911307a", + [] + ], + "highlight-currentcolor-painting-properties-001-ref.html": [ + "d3a3d49797a0ed723efe8024b37d400202df6886", + [] + ], + "highlight-currentcolor-painting-properties-002-ref.html": [ + "6f9066cba8a4108f9285fc427812cb57ebdb6a66", + [] + ], + "highlight-currentcolor-painting-text-shadow-001-ref.html": [ + "1ab60db5c8ec47e2ae2765a57ecfccd7f7d14fc5", + [] + ], + "highlight-currentcolor-painting-text-shadow-002-ref.html": [ + "6452a34d0e7826d0795c782ba39d7cd50c206cf2", + [] + ], + "highlight-currentcolor-root-explicit-default-001-ref.html": [ + "70ce6b59a49a000dd30cd03f36910bad9f67a7a1", + [] + ], + "highlight-currentcolor-root-explicit-default-002-ref.html": [ + "b64b007c64b66c556efe33c865468e10b1b4a887", + [] + ], + "highlight-currentcolor-root-implicit-default-ref.html": [ + "67ecb8df640f72e1725e42a648f2c6e1f33490e2", + [] + ], + "highlight-paired-cascade-001-ref.html": [ + "102b07b4814b6092f0307b8374997cb8edbb25be", + [] + ], + "highlight-paired-cascade-002-ref.html": [ + "19d731af02c99248ce321b4a3ddd36ac6af5ac01", + [] + ], + "highlight-paired-cascade-003-ref.html": [ + "638ae00c6858d7971a74ffbbc1b12e80f90fd849", + [] + ], + "highlight-paired-cascade-004-notref.html": [ + "72490b1461d6d4008edec59dd53b12f686a14c2d", + [] + ], + "highlight-paired-cascade-005-ref.html": [ + "862d06e6f648b5b4feeb5ec3294b605af1322dda", + [] + ], + "highlight-paired-cascade-006-ref.html": [ + "638ae00c6858d7971a74ffbbc1b12e80f90fd849", + [] + ] + }, + "highlight-custom-properties-dynamic-001-ref.html": [ + "836874df056a8a934c38aeebd7ace9d404b312b1", [] ], "highlight-painting-001-ref.html": [ @@ -409256,32 +409713,16 @@ "39d26e8387204840d89318ef917d5df0c4021df4", [] ], - "highlight-painting-soft-hyphens-001-ref.html": [ - "f1485ca9226683ac303facb5dadbd38b475c042e", - [] - ], - "highlight-paired-cascade-001-ref.html": [ - "14687acb841d2b85becead81a0c82b4e4b5b8e9d", - [] - ], - "highlight-paired-cascade-002-ref.html": [ - "48eb9911a104c7b74bbafaefbe19cd23c3ce5d7f", - [] - ], - "highlight-paired-cascade-003-ref.html": [ - "18885fdc898a4ccc260db6056f7c102ffd661c69", + "highlight-painting-shadows-horizontal-ref.html": [ + "4ff90afb388154da1b2f2ef62642f0480fa0e446", [] ], - "highlight-paired-cascade-004-notref.html": [ - "63472b67589c2b2bfd1c74d40b3dd0974e1a0e83", + "highlight-painting-shadows-vertical-ref.html": [ + "00a6f1c80885d61fdada7ea17f8376b63ec14090", [] ], - "highlight-paired-cascade-005-ref.html": [ - "0ac5c02b203bc0b84438e960cd5d015e47303982", - [] - ], - "highlight-paired-cascade-006-ref.html": [ - "18885fdc898a4ccc260db6056f7c102ffd661c69", + "highlight-painting-soft-hyphens-001-ref.html": [ + "f1485ca9226683ac303facb5dadbd38b475c042e", [] ], "highlight-styling-001-ref.html": [ @@ -409565,10 +410006,6 @@ "17d69f306d77411f2ef0a7dab682b119c5b59e50", [] ], - "cascade-highlight-005-ref.html": [ - "d1e597e05b8434bbb9b769cc62841662df0481b1", - [] - ], "first-letter-digraph-ref.html": [ "4eef817d7062a58066cf952c0c7f830f0fc87d37", [] @@ -409742,6 +410179,14 @@ "bd94d694359b4bac1e226d3520af3090c30c7ebe", [] ], + "target-text-shadow-horizontal-ref.html": [ + "6189f844b5ecb06ffd1fae67aeec4a28a5d7cf9c", + [] + ], + "target-text-shadow-vertical-ref.html": [ + "cd9e17905371bad10e38afcf45b6153992018fb3", + [] + ], "target-text-text-decoration-001-ref.html": [ "0ee37b1e8aadfed3945bfecc6dbe77e783def5a1", [] @@ -410514,6 +410959,10 @@ "11943c229a0ea76893039b1306bb38402c71f6ad", [] ], + "part-after-combinator-invalidation-ref.html": [ + "d6db0d0e4c4461a7f7f873f2f243d830b82f1afb", + [] + ], "part-nested-pseudo-ref.html": [ "8eee65a27045ae97d10ed22050126d94fa69ed9b", [] @@ -411401,7 +411850,7 @@ [] ], "2x2-green.webm": [ - "74af43afebe3468ff7868081ad3fc9b6f6417da8", + "d1c021c03d7d3559e2f169d7e55d28c4e4b1b36f", [] ] } @@ -420254,6 +420703,10 @@ "a2e9fd0d188940cd1f6b198480141cde59801b97", [] ], + "calc-rounding-003-ref.html": [ + "e80dd7b95c88255986e06f07cdd7b1edfee8e0fa", + [] + ], "calc-text-indent-1-ref.html": [ "71c0b48d6eee5eb7a09b440f934e24d1f912e680", [] @@ -421252,6 +421705,10 @@ "2927b468d08d452afc6a66267bf9786a6b00498e", [] ], + "pseudo-element-overflow-hidden-ref.html": [ + "02bcb5bb49c09ba151b10751ad31b9ebba91bdef", + [] + ], "pseudo-element-preserve-3d-ref.html": [ "1eefed24b3c0d10b2d073958a6da9963f793f53d", [] @@ -424006,6 +424463,10 @@ "3250801f64b5d6b4d430083bdc06c09aa0844370", [] ], + "WEB_FEATURES.yml": [ + "def314c45c7988cd00180f6687d6fc296ded7aad", + [] + ], "insertRule-from-script-ref.html": [ "2fa45526a66565f86832f843ec477fe76e496775", [] @@ -425318,6 +425779,14 @@ "f277097263e7acf6f2e7b40fa810231c61b00e63", [] ], + "WEB_FEATURES.yml": [ + "88f1510e1b4670cce079a4b48a62cd723693f095", + [] + ], + "prefers-color-scheme-svg-as-image-ref.html": [ + "012513ef9fafbd967b317f96c33492e049c6aeb7", + [] + ], "prefers-color-scheme-svg-image-normal-ref.html": [ "ef7fa1b366d3a6b9a5a578c542fb864238b55859", [] @@ -425765,7 +426234,7 @@ [] ], "WEB_FEATURES.yml": [ - "4b29277607ff59f605698b5ab133669a6c7e5879", + "2449bf4808f60c035717f1773c72ddd52df1c087", [] ], "attribute-selectors": { @@ -427954,7 +428423,7 @@ [] ], "xml2html.xsl": [ - "07b967500ff66c232aaefae1b240940bffbed969", + "88b74a9620f8093dcd5ff2afa4d1604793f049d9", [] ] }, @@ -428103,7 +428572,7 @@ [] ], "delete.js": [ - "131c99b1d5af0fcef45c270ea76596bc31af12a0", + "c4d1225ef35bad1638afe112b2741582e3b5ac47", [] ], "fontname.js": [ @@ -428123,7 +428592,7 @@ [] ], "forwarddelete.js": [ - "ea590a4fbba137fa1b265f15effe5bd03a7f2225", + "a881fb6ccf4618c16c3ac782bd5de1d1fb5a7aa9", [] ], "hilitecolor.js": [ @@ -428235,7 +428704,7 @@ }, "include": { "editor-test-utils.js": [ - "d0d50d22a666b7d7229429eed7a0cc683aa005a5", + "b180f3343fde2189ea6e122266cf329a2e2b6d98", [] ], "implementation.js": [ @@ -431187,6 +431656,10 @@ "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", [] ], + "huge-response.py": [ + "16a60078e53f5a9deba88283eecd2ce204467562", + [] + ], "infinite-slow-response.py": [ "a26cd8064c88531e6877d2561cd16df964eb7f6e", [] @@ -431934,7 +432407,7 @@ [] ], "fetch-metadata.conf.yml": [ - "b277bcb7b53d54c1cb6aac02096cc287b6438891", + "11e6140343638fd47c6fe6795d9e6127ae41cf40", [] ], "generate.py": [ @@ -431942,10 +432415,6 @@ [] ], "templates": { - "appcache-manifest.sub.https.html": [ - "0dfc084f2e3442038bc4eaf62a88f514053a6b97", - [] - ], "audioworklet.https.sub.html": [ "7be309c50685ec28d3f7541ce5568767e2ff0054", [] @@ -436864,22 +437333,10 @@ "2733836c998fa919d9f6880fd96a32026e82ca2c", [] ], - "2d.fillStyle.parse.hsl-clamp-1.png": [ - "2733836c998fa919d9f6880fd96a32026e82ca2c", - [] - ], - "2d.fillStyle.parse.hsl-clamp-2.png": [ + "2d.fillStyle.parse.hsl-clamp-negative-saturation.png": [ "88fd8279855266c96740bc935718b22db592198c", [] ], - "2d.fillStyle.parse.hsl-clamp-3.png": [ - "bf48767a881289c077718965163addae848790ac", - [] - ], - "2d.fillStyle.parse.hsl-clamp-4.png": [ - "d638d033868afd3aab0a7b26d5b2a0a2b95da010", - [] - ], "2d.fillStyle.parse.hsla-1.png": [ "2aa6265f066dac5123d5452032c3e88052f035f7", [] @@ -436888,28 +437345,16 @@ "2733836c998fa919d9f6880fd96a32026e82ca2c", [] ], - "2d.fillStyle.parse.hsla-clamp-1.png": [ + "2d.fillStyle.parse.hsla-clamp-alpha-1.png": [ "2733836c998fa919d9f6880fd96a32026e82ca2c", [] ], - "2d.fillStyle.parse.hsla-clamp-2.png": [ - "88fd8279855266c96740bc935718b22db592198c", - [] - ], - "2d.fillStyle.parse.hsla-clamp-3.png": [ - "bf48767a881289c077718965163addae848790ac", - [] - ], - "2d.fillStyle.parse.hsla-clamp-4.png": [ - "d638d033868afd3aab0a7b26d5b2a0a2b95da010", - [] - ], - "2d.fillStyle.parse.hsla-clamp-5.png": [ - "2733836c998fa919d9f6880fd96a32026e82ca2c", + "2d.fillStyle.parse.hsla-clamp-alpha-2.png": [ + "eeedd0ff05889ffd4468bf19a2e8e9e0a094201c", [] ], - "2d.fillStyle.parse.hsla-clamp-6.png": [ - "eeedd0ff05889ffd4468bf19a2e8e9e0a094201c", + "2d.fillStyle.parse.hsla-clamp-negative-saturation.png": [ + "88fd8279855266c96740bc935718b22db592198c", [] ], "2d.fillStyle.parse.html4.png": [ @@ -438154,6 +438599,10 @@ "af9c736aea7f9b20f4fd09522a8aa2dd25a11ef1", [] ], + "WEB_FEATURES.yml": [ + "1d9e4bab82191d72c374c7399e666bd6bfe42bc2", + [] + ], "reference": { "direction-default-ref.html": [ "cef6df259c4801e93e7ce8dd1692e6f3157d77bc", @@ -438668,6 +439117,10 @@ "af9c736aea7f9b20f4fd09522a8aa2dd25a11ef1", [] ], + "WEB_FEATURES.yml": [ + "1d9e4bab82191d72c374c7399e666bd6bfe42bc2", + [] + ], "canvas.2d.fontStretch-ref.html": [ "00ecdccad3743db0d0a1ec99850f80ea7964d512", [] @@ -438814,7 +439267,7 @@ "yaml": { "element": { "meta.yaml": [ - "5fd8b68498ac3fa8e8bc79d58738c503e3517193", + "12852e200ad7199f25cc11d877fc5a62df2abf75", [] ], "the-canvas-element.yaml": [ @@ -438824,7 +439277,7 @@ }, "offscreen": { "meta.yaml": [ - "7b44fd9f2677fc63ce974a426025f6646cf9e3c8", + "b07898224d2b3c3cc2e038dcf23589c26071df02", [] ], "the-offscreen-canvas.yaml": [ @@ -442338,10 +442791,6 @@ "d6aaa18ad76a540e019a5c6eb2b8a9868a785c51", [] ], - "dragdrop_support.js": [ - "f5a1d6417f313c08185131786d23906fde2aca6a", - [] - ], "fail.png": [ "b5933803338f770bdb1e6a7d433aeb640be85b08", [] @@ -447248,7 +447697,7 @@ [] ], "bounded-sizes-reftest-ref.html": [ - "1e0104cc5ce6963f77cdc1a1670ec0cdf234c195", + "b186dd644581822f288d3c756d825203e2ed8330", [] ], "display-css-property-reftest-ref.html": [ @@ -449055,7 +449504,7 @@ ], "pseudo-classes": { "WEB_FEATURES.yml": [ - "604e049f202c94d9bd55edefd7b09f57cd9fedf6", + "055a5fb4a302b72585d2ca80a849bd653a4d0666", [] ], "focus-iframe.html": [ @@ -451604,6 +452053,12 @@ [] ] }, + "inert": { + "WEB_FEATURES.yml": [ + "074e33726c62121956e2d63027342c711147a1c4", + [] + ] + }, "infrastructure": { "META.yml": [ "64a240ccbe8116a06ec40fd03140e6bbee7a260f", @@ -452164,7 +452619,7 @@ [] ], "DOM-Parsing.idl": [ - "d0d84ab6972f02ae11f2e3a850749064d63fa299", + "676753bf0fce08b7fb8a0e85564016a4c749ea75", [] ], "EXT_blend_minmax.idl": [ @@ -452440,7 +452895,7 @@ [] ], "compute-pressure.idl": [ - "c4dcb90af4321e08b41e87a7d6d5bb54922bd2e0", + "a90febffc3b018571c1ae3ff78a8e6a56de7cc90", [] ], "console.idl": [ @@ -452476,7 +452931,7 @@ [] ], "css-anchor-position.idl": [ - "b79e3fce893ddc13bc89324bacea4340777d09e6", + "5eeaa030b85d9edd6da1daf69c3dc7e7410fc86f", [] ], "css-animation-worklet.idl": [ @@ -452547,6 +453002,10 @@ "03f039e4cb54f5f714bc252e7bfbd609ec769ed2", [] ], + "css-nesting.idl": [ + "58d3247f9048b6a2ba1dedb2954ef30943f89de2", + [] + ], "css-paint-api.idl": [ "0924c535566779e4f20ee89360df3c334943022f", [] @@ -452588,7 +453047,7 @@ [] ], "css-view-transitions-2.idl": [ - "41337f4e1e427f63cab3cea259545bfc08ace39b", + "559870751a20ae7b378d4ffefb8eec30cce2eddd", [] ], "css-view-transitions.idl": [ @@ -452611,6 +453070,10 @@ "4cf76ba8114723a7ee030a81c86e1549c76b6760", [] ], + "device-attributes.idl": [ + "cf62523ad828668519ff0a572d09a4365696a03c", + [] + ], "device-memory.idl": [ "e8197e8497d2ec48e1507284b4f823de820204f7", [] @@ -452624,15 +453087,15 @@ [] ], "digital-identities.idl": [ - "9027ce61af8175a78fdd4a6b7ca41324756c8b85", + "2d1b7208502e040bbf4080db7ae1404c778e4dba", [] ], "document-picture-in-picture.idl": [ - "888855b38f2d0d4881974b7f7a6703ca674e4847", + "ed34b3c21607e6bc5398f96605a63d14a9a76c25", [] ], "dom.idl": [ - "cf2d4e4adc58319842b0e7873c012cf4a8777f39", + "72d61f5cfd80ad446e4508b8e8bfe29c424d27c4", [] ], "edit-context.idl": [ @@ -452704,7 +453167,7 @@ [] ], "gamepad.idl": [ - "024e5ea58c1c33ed4c9a0b94ddf53317fa43936b", + "d922d7b80b05dd490281b7e3be5530a05fdee076", [] ], "generic-sensor.idl": [ @@ -452740,7 +453203,7 @@ [] ], "html.idl": [ - "2f97e4dd603e0d5947144e41a80c90281a02b799", + "e9598a1bbb60a2d43e612d8642100db273157d8b", [] ], "idle-detection.idl": [ @@ -452876,7 +453339,7 @@ [] ], "mediasession.idl": [ - "8e9a21aff063a7b6d5f34f59cb293c493ffb8c4c", + "e6c8e46462793cf6e96bebe48c04c466f8cc5117", [] ], "mediastream-recording.idl": [ @@ -453044,7 +453507,7 @@ [] ], "sanitizer-api.idl": [ - "599d8f82ea45eb2e04bad0c82f15982a183b6394", + "8f5c667973a359ed576c143b0a5aa639c0a89b94", [] ], "sanitizer-api.tentative.idl": [ @@ -453096,15 +453559,15 @@ [] ], "service-workers.idl": [ - "c740e1098a884726b28dbd07cfe12bbebf2f1a00", + "1ddc6d71d83d9ffd5acf3ceea52f9fc4c31ba69b", [] ], "shape-detection-api.idl": [ - "4fc1f085ea2c24b73a9fa5f7d706d87fedf225a6", + "24d3b980854f57f3ce04d59d2595e5b66cdbb023", [] ], "shared-storage.idl": [ - "edbe2c2bcc3e913ff42575fe8ffef7e2425051d9", + "c40344e74d2c7f9aac8a573fc267fa548ffffb5d", [] ], "speech-api.idl": [ @@ -453136,7 +453599,7 @@ [] ], "text-detection-api.idl": [ - "95b642749f73a63d62d5aa5320815aeb7091be4b", + "b6745b18754e8a73e48b2acef832665ed8a26147", [] ], "touch-events.idl": [ @@ -453148,11 +453611,11 @@ [] ], "trusted-types.idl": [ - "db5bd635cf9f699a6a8628898883d79a34336255", + "a0f88e4e6c38cab683a236f3e8fa211b7e4e2107", [] ], "turtledove.idl": [ - "2547e1fb549ca21b94921b043d53b56915cd2f66", + "39e90ddae19e2bb1a2372cbadd7c15a8e2f7bfe1", [] ], "ua-client-hints.idl": [ @@ -453196,7 +453659,7 @@ [] ], "wasm-js-api.idl": [ - "0d4384251df458a13e0c923c5412848039903da4", + "b4f723d050af7d9fb88f6f760d3237b74aaec41c", [] ], "wasm-web-api.idl": [ @@ -453276,7 +453739,7 @@ [] ], "webcodecs.idl": [ - "371546eb0d05d2a56cc8f726b2e6d36359d450f3", + "19964c51e45efddd9c004a28aaa202842f211a2a", [] ], "webcrypto-secure-curves.idl": [ @@ -453304,7 +453767,7 @@ [] ], "webidl.idl": [ - "dff46c557cb4c608ba2c1ead01fb025fbcb1ff8c", + "f3db91096ac1be7558631ed9b7afd7a4dac6706e", [] ], "webmidi.idl": [ @@ -453312,11 +453775,11 @@ [] ], "webnn.idl": [ - "0b8ea7cb3440a2b016643669bb976a11c0c095a5", + "9beb8858d102ddabab04ec6e6670722347c29862", [] ], "webrtc-encoded-transform.idl": [ - "8a756702c7eaddda600a1e2848f6e62d0413d24b", + "0db2f2b9a81a9d9f6525ff60b533434f3fe5d3c7", [] ], "webrtc-ice.idl": [ @@ -453340,7 +453803,7 @@ [] ], "webrtc.idl": [ - "e571abb527f843a9b0f93e1e093a1989dce6938e", + "65e7aa622c5381a1e3d888dd2c4aef04b199aeba", [] ], "websockets.idl": [ @@ -453396,7 +453859,7 @@ [] ], "webxr.idl": [ - "3b7f8a55b7c63a1183d002baa3646064084932cc", + "8e02fbd38a03c77a70ce059278993c09815d54b6", [] ], "webxrlayers.idl": [ @@ -461276,7 +461739,7 @@ }, "screen-details": { "META.yml": [ - "abfee89ffbdd29ccd2741bbd3d4a314387025509", + "2e3284c67cc049ff02b17b1c2205a0ce729b3528", [] ] }, @@ -464448,7 +464911,7 @@ "focus-navigation": { "resources": { "focus-utils.js": [ - "0392cfb88792ee66d93eb7f1430fa6afd846c858", + "f4056dc1688442c2c41ce9421161bea448da7c2f", [] ], "shadow-dom.js": [ @@ -464865,7 +465328,11 @@ [] ], "simple-module.js": [ - "620a3592f27fc89eb5df88bc05a6e1df1d854975", + "11b650811dcc9cea02d541a72314ea84a335f3fb", + [] + ], + "simple-module.js.headers": [ + "cf3e03e24c7d68e3fb9f0be9102591c659a2b43c", [] ], "util.js": [ @@ -467290,7 +467757,7 @@ [] ], "install_chrome.yml": [ - "7599321be24877788efd8aa970f318ff5c7d2f70", + "9b03d9b91bcf9bb9ca100ceb244ee83e3b001a6b", [] ], "install_edge.yml": [ @@ -467391,15 +467858,15 @@ [] ], "requirements_build.txt": [ - "54f21efbd98ba0ce5b7823cc31205f3fbcebf238", + "7b4f8619b2e5d3bbbf3fa1d15de96c76fcc44e98", [] ], "requirements_macos_color_profile.txt": [ - "7505a98d9f3c27add68ed8e1fe17ceae2358e91f", + "8e178d1d2c464d1ed69b3e4d21cffa482b5bc618", [] ], "requirements_tc.txt": [ - "e1ae4dbf70ed5602e318fdfe5f95bb149ac398aa", + "aa57643b9b58af506ca3548239adda1d4ff35534", [] ], "run_tc.py": [ @@ -467827,7 +468294,7 @@ [] ], "item.py": [ - "86f7bd6020b4e44f21359566fb65f4fe9ae3d775", + "e25f7ca2c29adc3cd9165992117b37bece5fc312", [] ], "jsonlib.py": [ @@ -467851,7 +468318,7 @@ [] ], "sourcefile.py": [ - "6da919b3b668a47aa48ce47383567588fd3af88d", + "3563fb9e5e90d673f9f024f2affbde38365246d6", [] ], "spec.py": [ @@ -467998,19 +468465,19 @@ [] ], "requirements_flake8.txt": [ - "fc1f92a69f703bfe5e340c44e0c9022f45c97ae2", + "3f7f3121ca270d5fc43ad959d6e181017ce2f413", [] ], "requirements_mypy.txt": [ - "c3db2292af599ad62a749c6d7baa3fd75136e48e", + "3b1d3b03d6445c4324c298c7a034b42bb847be01", [] ], "requirements_pytest.txt": [ - "9034cda71908a5d6f2744b122b001d8a037459e0", + "64d38583a2fd682ae5d12ec0eac8e9f0711bc1af", [] ], "requirements_tests.txt": [ - "6455286736da2bbc07783da804e1990a9fb34022", + "2613def3da422e79e55d82f295609fe8ffc8c4ab", [] ], "runner": { @@ -476173,58 +476640,932 @@ } }, "websockets": { + ".github": { + "FUNDING.yml": [ + "c6c5426a5a6df9669b0cb291fe3aa8cd88887a94", + [] + ], + "ISSUE_TEMPLATE": { + "config.yml": [ + "3ba13e0cec6cbbfd462e9ebf529dd2093148cd69", + [] + ], + "issue.md": [ + "3cf4e3b77011e66618b9275f8367179709557c5a", + [] + ] + }, + "dependabot.yml": [ + "ad1e824b4a5bc06eee3b004379b7ccdd32fc2bc2", + [] + ], + "workflows": { + "tests.yml": [ + "470f5bc96063142a155a2d6c5830883011c3a20d", + [] + ], + "wheels.yml": [ + "707ef2c60d1db743169b15373b3b9163702133fc", + [] + ] + } + }, + ".gitignore": [ + "324e77069a9581927e71f48230c975de9765c05a", + [] + ], + ".readthedocs.yml": [ + "0369e0656548151a22b18ec614eeda592f00c5af", + [] + ], + "CODE_OF_CONDUCT.md": [ + "80f80d51b115134c79329b69f02c47e3906e0f59", + [] + ], "LICENSE": [ - "119b29ef35b9f8149cc383bc751ba4d10483010c", + "5d61ece22a75a759aed8e52af280eca28d35d6bf", [] ], "MANIFEST.in": [ "1c660b95b1464a9401ecd506323158ea2cb6bbfd", [] ], - "PKG-INFO": [ - "3b042a3f9f822cace1abce4264b479420920fb48", + "Makefile": [ + "cf3b533939418324edc420cb8112f64390d3965d", [] ], "README.rst": [ - "2b9a445ea5c9ff93c27ae2f3ee44a58b437b85e1", + "870b208baae0aaee0b1de417d355145426ccc708", [] ], - "setup.cfg": [ - "dc424fe1955179df2b9b9a382ab840869f7dc5e4", + "SECURITY.md": [ + "175b20c589ded2c7c824a4484de9b7cf4e9367ef", + [] + ], + "compliance": { + "README.rst": [ + "8570f9176d56b47b38852222e490bdc74bea8ead", + [] + ], + "fuzzingclient.json": [ + "202ff49a03a59ab707090ce56bba1ffcea1281cf", + [] + ], + "fuzzingserver.json": [ + "1bdb42723ef92c37a08871c5a9dc0ae943d15a0f", + [] + ], + "test_client.py": [ + "1ed4d711e925dea3cb515399f3ab5f76b270126f", + [] + ], + "test_server.py": [ + "92f895d92695ead26d16bea27b0e4dbbc1fb022e", + [] + ] + }, + "docs": { + "Makefile": [ + "0458706458855d9fa48dae9b3d0eca001ebe3c7c", + [] + ], + "_static": { + "favicon.ico": [ + "36e855029d705e72d44428bda6e8cb6d3dd317ed", + [] + ], + "tidelift.png": [ + "317dc4d9852df72ba34e10a6f61d1838cbbd969e", + [] + ], + "websockets.svg": [ + "b07fb22387385d41d6e1050e603641ea1fe54e12", + [] + ] + }, + "conf.py": [ + "9d61dc717369b49fc05801872e12ae5dd137a9c3", + [] + ], + "faq": { + "asyncio.rst": [ + "e77f50adddcdbb24da419bcee062b1627eb64d33", + [] + ], + "client.rst": [ + "c590ac107db3ad8c242181955c5c2543fc7cb9b9", + [] + ], + "common.rst": [ + "2c63c4f36f1152db6cc36d95209e62249e8aa6ee", + [] + ], + "index.rst": [ + "9d5b0d538ac773f22c8a2a13e3500633fef9f962", + [] + ], + "misc.rst": [ + "ee5ad23728e1497815736db236b136e460ed643f", + [] + ], + "server.rst": [ + "08b412d306b954c75b8d7cfd4478611ed63438d2", + [] + ] + }, + "howto": { + "autoreload.rst": [ + "fc736a59186c4ee680a22575bc7a28005a721f7d", + [] + ], + "cheatsheet.rst": [ + "95b551f6731ddaedd2b66e31e1735a67fbec4b08", + [] + ], + "django.rst": [ + "e3da0a878b83db0c6bf81f58b478a95b4f22fdb2", + [] + ], + "extensions.rst": [ + "3c8a7d72a64a8f11b95279caeeafd598150a0b85", + [] + ], + "fly.rst": [ + "ed001a2aeed9f3a34236b464341071cde4b2ad4d", + [] + ], + "haproxy.rst": [ + "fdaab04011c9d45be9be91a74f98d4456b27a389", + [] + ], + "heroku.rst": [ + "a97d2e7ce039375eaf346cfd196a80f530dd8aa9", + [] + ], + "index.rst": [ + "ddbe67d3ae20869a242ca639872c921a9eabd56a", + [] + ], + "kubernetes.rst": [ + "064a6ac4d58014436fcc3ae94d619c27695e0432", + [] + ], + "nginx.rst": [ + "30545fbc7d1ca23a9e09f03ac4bf023ca4f1abd9", + [] + ], + "patterns.rst": [ + "c6f325d21372f3903114ba93ed09ba568ad868f0", + [] + ], + "quickstart.rst": [ + "ab870952c17618ae1f0b9a0e23c1f80140712f81", + [] + ], + "render.rst": [ + "70bf8c376c4275e98734f424cb11f4b09ffe7517", + [] + ], + "sansio.rst": [ + "d41519ff09157d06896a5ed229824da28cf37fbd", + [] + ], + "supervisor.rst": [ + "5eefc7711b80a8bdd5865f6a3443a352d19e7b89", + [] + ] + }, + "index.rst": [ + "d9737db12a69b1471bb6969e3062fa4098fc0593", + [] + ], + "intro": { + "index.rst": [ + "095262a2073fff410d1885545e193a4f8ac4d7bc", + [] + ], + "tutorial1.rst": [ + "ff85003b58a0b11112f60b9b7dff72bddefb1289", + [] + ], + "tutorial2.rst": [ + "5ac4ae9dd5501b117c0105dfb5ccaaa8c8072f02", + [] + ], + "tutorial3.rst": [ + "6fdec113b2ad92bcfb0235566b8e873b5c83d94e", + [] + ] + }, + "make.bat": [ + "2119f51099bf37e4fdb6071dce9f451ea44c62dd", + [] + ], + "project": { + "changelog.rst": [ + "264e6e42d1c1c2c8b80ee09d1604e012a268b02d", + [] + ], + "contributing.rst": [ + "020ed7ad85cd1ee515d114d503c57ffa1e74fbd5", + [] + ], + "index.rst": [ + "459146345b0d8dc4f3edc807009b59aef8a2a721", + [] + ], + "license.rst": [ + "0a3b8703d568a951119da2630fb387fb9fb83854", + [] + ], + "tidelift.rst": [ + "42100fade9b2ab8abcfd6bb3876befca893affdf", + [] + ] + }, + "reference": { + "asyncio": { + "client.rst": [ + "5086015b7b9825d1d896cd3916afa2a98f917acb", + [] + ], + "common.rst": [ + "dc7a54ee1abc9434c72c5cc9bc5472a86eca49f1", + [] + ], + "server.rst": [ + "10631791626c9a39a5526d55dd392a0007b46a9a", + [] + ] + }, + "datastructures.rst": [ + "ec02d421015c8840753507a94255ca20e67170eb", + [] + ], + "exceptions.rst": [ + "907a650d204ba98b10a9377cff29c06d59dd1bfd", + [] + ], + "extensions.rst": [ + "a70f1b1e58a76c7a519d2bad21af6ec696be96ab", + [] + ], + "features.rst": [ + "98b3c0ddafcabbf3fc1de87a75cd5c614d9553c6", + [] + ], + "index.rst": [ + "0b80f087a1f4f82c434a4b76738f3dfcefe26940", + [] + ], + "sansio": { + "client.rst": [ + "09bafc74558dda5fef566c735a72f4856dc3700a", + [] + ], + "common.rst": [ + "cd1ef3c63a585203cfadab0a760372f32d7bc1b4", + [] + ], + "server.rst": [ + "d70df6277a4eba0629cc591ce4f712969a5ff03f", + [] + ] + }, + "sync": { + "client.rst": [ + "6cccd6ec486c47688bee0c1b86c689a55592e23d", + [] + ], + "common.rst": [ + "3dc6d4a50967299c09a120e01f43e4e198687e74", + [] + ], + "server.rst": [ + "35c112046acbb7b0a8e3af3022d76dd98aaec59a", + [] + ] + }, + "types.rst": [ + "9d3aa8310bc6c4c6db3b785f1a39166019e12b22", + [] + ] + }, + "requirements.txt": [ + "bcd1d711435f9fcfbd5fa0f2aa044abc18dd91bd", + [] + ], + "spelling_wordlist.txt": [ + "dfa7065e79e18270463d89c0baceb6cfcee1f3e2", + [] + ], + "topics": { + "authentication.rst": [ + "31bc8e6da8a14dc0ab931fb8df2991f89991b96d", + [] + ], + "authentication.svg": [ + "ad2ad0e442881b856ea26c8d3120084add1d898b", + [] + ], + "broadcast.rst": [ + "1acb372d4f3ae03ccaeb2acb5210f80b7ec43197", + [] + ], + "compression.rst": [ + "eaf99070db015b5064f14599b988a9e806d1c262", + [] + ], + "data-flow.svg": [ + "749d9d482d2f01563c0226f96042b03b3b3521c7", + [] + ], + "deployment.rst": [ + "2a1fe9a78503627997051c1ad78441e1e5b4e530", + [] + ], + "deployment.svg": [ + "fbacb18c4b7e33f624036eb5fe7ed16d59a4a6eb", + [] + ], + "design.rst": [ + "f164d29905ee7b982232464143141ea2e3145ae3", + [] + ], + "index.rst": [ + "120a3dd3277c9b048e98fdc22d9f9fd460b975c7", + [] + ], + "lifecycle.graffle": [ + "a8ab7ff09f52a41e3e8a8abc2e8a5503e481c44a", + [] + ], + "lifecycle.svg": [ + "0a9818d2930c50f638ade86a2f16fafd254a13dd", + [] + ], + "logging.rst": [ + "e7abd96ce502b49d0f878b9f23aa2e4e6b0cc32e", + [] + ], + "memory.rst": [ + "e44247a77c5fc96571addcc868b0de53c3b2fe35", + [] + ], + "performance.rst": [ + "45e23b2390a3cd427b7c6737059bdb5aad3f4442", + [] + ], + "protocol.graffle": [ + "df76f49607e5743b809e6f41787262932b6b0743", + [] + ], + "protocol.svg": [ + "51bfd982be7bac755c318376e79b6f7d3dfd91ee", + [] + ], + "security.rst": [ + "d3dec21bd103976e293325b287aeb1e244e9feb4", + [] + ], + "timeouts.rst": [ + "633fc1ab4314544e12d43440e059e0dbc7b422d3", + [] + ] + } + }, + "example": { + "deployment": { + "fly": { + "Procfile": [ + "2e35818f675d5a1a7b6a84e4561bec032180c1d6", + [] + ], + "app.py": [ + "4ca34d23bbedeffb6f8cde4eef6de99dea8327a9", + [] + ], + "fly.toml": [ + "5290072ed28c6b804295c6028e300e958d515fc2", + [] + ], + "requirements.txt": [ + "14774b465e97f655dbcaa60d97c8a9aa72e7d51b", + [] + ] + }, + "haproxy": { + "app.py": [ + "360479b8eb65558d34d9fe777b642500364ad4b2", + [] + ], + "haproxy.cfg": [ + "e63727d1c0e5b813e9cc8c43bab375e66289d28c", + [] + ], + "supervisord.conf": [ + "76a664d91b12dead5d4fc196a62095172986fc17", + [] + ] + }, + "heroku": { + "Procfile": [ + "2e35818f675d5a1a7b6a84e4561bec032180c1d6", + [] + ], + "app.py": [ + "d4ba3edb51110356a38dd74979b23680dda8411f", + [] + ], + "requirements.txt": [ + "14774b465e97f655dbcaa60d97c8a9aa72e7d51b", + [] + ] + }, + "kubernetes": { + "Dockerfile": [ + "83ed8722c0713451e97b6b33450bc6b10672dec1", + [] + ], + "app.py": [ + "a8bcef68813c93b2a89ed0d727e0887a9bcd4f40", + [] + ], + "benchmark.py": [ + "22ee4c5bd7b9d10f846cca2d41035fc3d8e112ca", + [] + ], + "deployment.yaml": [ + "ba58dd62bf4e7443025f31c916ecea56960f4142", + [] + ] + }, + "nginx": { + "app.py": [ + "24e60897562fa0eba0092f339d41a3d00fb63e47", + [] + ], + "nginx.conf": [ + "67aa0086d54f3dd0fd230b6ff6fb68bf502e9b09", + [] + ], + "supervisord.conf": [ + "76a664d91b12dead5d4fc196a62095172986fc17", + [] + ] + }, + "render": { + "app.py": [ + "4ca34d23bbedeffb6f8cde4eef6de99dea8327a9", + [] + ], + "requirements.txt": [ + "14774b465e97f655dbcaa60d97c8a9aa72e7d51b", + [] + ] + }, + "supervisor": { + "app.py": [ + "bf61983ef7b7a071a8e54b88a56271021c971f23", + [] + ], + "supervisord.conf": [ + "76a664d91b12dead5d4fc196a62095172986fc17", + [] + ] + } + }, + "django": { + "authentication.py": [ + "f6dad0f55ef01d8cf6715eaba0d8e8c894c54499", + [] + ], + "notifications.py": [ + "3a9ed10cf09665e071f4228003b2d2a0d3c3f565", + [] + ], + "signals.py": [ + "6dc827f72d70f1e63a542ecfc7764d43a8a7cd47", + [] + ] + }, + "echo.py": [ + "2e47e52d949bee9f4ae03447284962635f72de83", + [] + ], + "faq": { + "health_check_server.py": [ + "7b8bded772068d7e6a938efeb8142a43f963b078", + [] + ], + "shutdown_client.py": [ + "539dd0304a9c2b566cd67c38c595f39b261417fb", + [] + ], + "shutdown_server.py": [ + "1bcc9c90baa04078388102e07e323a6d10ae1847", + [] + ] + }, + "hello.py": [ + "a3ce0699ee4c6147e79b78dbfd0883931018ea16", + [] + ], + "legacy": { + "basic_auth_client.py": [ + "164732152f6d44a3d0994cf85f9aa7ba099223a9", + [] + ], + "basic_auth_server.py": [ + "d2efeb7e5303ba6c3ec5aa410381bed75e56adc9", + [] + ], + "unix_client.py": [ + "92615673032dcb6b3035e7d73b0525fd86a1d6e3", + [] + ], + "unix_server.py": [ + "335039c351ca6cd1d82353487760a628f615c5d2", + [] + ] + }, + "logging": { + "json_log_formatter.py": [ + "b8fc8d6dc930f1a114536f7e26c79e3bba7ab572", + [] + ] + }, + "quickstart": { + "client.py": [ + "8d588c2b0e44806824c49536be91648c9e2878d1", + [] + ], + "client_secure.py": [ + "f4b39f2b838b10fc533a191a2422093205cc4c5a", + [] + ], + "counter.css": [ + "e1f4b77148bc4503cbec34929e8234a7f263c66d", + [] + ], + "counter.html": [ + "2e3433bd215baaa31f5604fa5b3a49288f46e6e9", + [] + ], + "counter.js": [ + "37d892a28b447e243262757c225204a48d60286e", + [] + ], + "counter.py": [ + "566e12965edde520d9831149f691ea93bba7347d", + [] + ], + "localhost.pem": [ + "f9a30ba8f63933635bf71f75ad8a6ac31745fdae", + [] + ], + "server.py": [ + "31b18297298bd27c046155e6b3010751a4cd7037", + [] + ], + "server_secure.py": [ + "de41d30dc053c3cbbdf0b2a158763a9b201e04e7", + [] + ], + "show_time.html": [ + "b1c93b141d2e410a145cc7d5e010dd31db608ea2", + [] + ], + "show_time.js": [ + "26bed7ec9e79abb6ce8c899ce12be92df69094b8", + [] + ], + "show_time.py": [ + "a83078e8a9111dfc7f9da57cb1189d95231089f4", + [] + ], + "show_time_2.py": [ + "08e87f5931a44fc0d771a682be503a6df783b094", + [] + ] + }, + "tutorial": { + "start": { + "connect4.css": [ + "27f0baf6e457f41eea2da6094a368817d5c001d2", + [] + ], + "connect4.js": [ + "cb5eb9fa27b550210dee43ff3988fe5069a35dfe", + [] + ], + "connect4.py": [ + "0a61e7c7ee1d66f6edc2ae4ea4cf87bd8e3cef8a", + [] + ], + "favicon.ico": [ + "36e855029d705e72d44428bda6e8cb6d3dd317ed", + [] + ] + }, + "step1": { + "app.py": [ + "3b0fbd786859759665affa346430cc340ec93454", + [] + ], + "connect4.css": [ + "27f0baf6e457f41eea2da6094a368817d5c001d2", + [] + ], + "connect4.js": [ + "cb5eb9fa27b550210dee43ff3988fe5069a35dfe", + [] + ], + "connect4.py": [ + "0a61e7c7ee1d66f6edc2ae4ea4cf87bd8e3cef8a", + [] + ], + "favicon.ico": [ + "36e855029d705e72d44428bda6e8cb6d3dd317ed", + [] + ], + "index.html": [ + "8e38e899222dc514daac37b47bc1bf8d0e5838cd", + [] + ], + "main.js": [ + "dd28f9a6a8bb682356d53f4c3f1d5ef4d1d56ba2", + [] + ] + }, + "step2": { + "app.py": [ + "2693d4304df50fb2dda5a8f62b1f55dc72ad67aa", + [] + ], + "connect4.css": [ + "27f0baf6e457f41eea2da6094a368817d5c001d2", + [] + ], + "connect4.js": [ + "cb5eb9fa27b550210dee43ff3988fe5069a35dfe", + [] + ], + "connect4.py": [ + "0a61e7c7ee1d66f6edc2ae4ea4cf87bd8e3cef8a", + [] + ], + "favicon.ico": [ + "36e855029d705e72d44428bda6e8cb6d3dd317ed", + [] + ], + "index.html": [ + "1a16f72a257bbe20df55e67a6e9f72fe0df022ac", + [] + ], + "main.js": [ + "d38a0140acc72fcae606110d294df42ba7ce9585", + [] + ] + }, + "step3": { + "Procfile": [ + "2e35818f675d5a1a7b6a84e4561bec032180c1d6", + [] + ], + "app.py": [ + "c2ee020d201ca9919045ef32a7c6daf4e3b931ff", + [] + ], + "connect4.css": [ + "27f0baf6e457f41eea2da6094a368817d5c001d2", + [] + ], + "connect4.js": [ + "cb5eb9fa27b550210dee43ff3988fe5069a35dfe", + [] + ], + "connect4.py": [ + "0a61e7c7ee1d66f6edc2ae4ea4cf87bd8e3cef8a", + [] + ], + "favicon.ico": [ + "36e855029d705e72d44428bda6e8cb6d3dd317ed", + [] + ], + "index.html": [ + "1a16f72a257bbe20df55e67a6e9f72fe0df022ac", + [] + ], + "main.js": [ + "3000fa2f78c4dc4a86ac34a61a3c90597168cb6a", + [] + ], + "requirements.txt": [ + "14774b465e97f655dbcaa60d97c8a9aa72e7d51b", + [] + ] + } + } + }, + "experiments": { + "authentication": { + "app.py": [ + "039e21174b169e04a7b970f958aca2b6f4545651", + [] + ], + "cookie.html": [ + "ca17358fd0d05c58696f4fd608c2eda192ec20e6", + [] + ], + "cookie.js": [ + "2cca34fcbb49cceccde7b7e3b33962e7f4455ff2", + [] + ], + "cookie_iframe.html": [ + "9f49ebb9a080e141eaa876afc10f3e4062ee4c74", + [] + ], + "cookie_iframe.js": [ + "2d2e692e8d252a1d296b1f3f862f69bdf340b863", + [] + ], + "favicon.ico": [ + "36e855029d705e72d44428bda6e8cb6d3dd317ed", + [] + ], + "first_message.html": [ + "4dc511a1764f2ebd575a005797af81493daf28b0", + [] + ], + "first_message.js": [ + "1acf048bafb3fdbca5a588bb7b6c3d5a82cc184e", + [] + ], + "index.html": [ + "c37deef2707b9878b1ab22c4d22eb292ab67435a", + [] + ], + "query_param.html": [ + "27aa454a4017002e78d11f77788134b9842849cd", + [] + ], + "query_param.js": [ + "6a54d0b6caeb5288cf4248c1b4c7d299deee8a00", + [] + ], + "script.js": [ + "ec4e5e6709bfc5ff1d9697dbb1dac51481a140d6", + [] + ], + "style.css": [ + "6e3918ccae60e096d8ac491c653911f85abbce22", + [] + ], + "test.html": [ + "3883d6a39e67c2ebd3c7cc9d65abf55d8a516fb9", + [] + ], + "test.js": [ + "428830ff31de45f37db49933d8375574004db050", + [] + ], + "user_info.html": [ + "7b9c99c730116f012f4ec5217c7fef973a5182e7", + [] + ], + "user_info.js": [ + "1dab2ce4c1709a743d527ab1142c26a293053072", + [] + ] + }, + "broadcast": { + "clients.py": [ + "fe39dfe051ee28d109a16ef979dc4c5157426f3c", + [] + ], + "server.py": [ + "9c9907b7f9e4213593cc2ebd758523cb25937ef7", + [] + ] + }, + "compression": { + "benchmark.py": [ + "c5b13c8fa343d2dae8a96a796ed3237905eb9c54", + [] + ], + "client.py": [ + "3ee19ddc59ab4453e1ae044eac8eefc58c0021ff", + [] + ], + "server.py": [ + "8d1ee3cd7c0d27b663818907a03bbd9205af653c", + [] + ] + }, + "optimization": { + "parse_frames.py": [ + "e3acbe3c20129ac9e4184b9f5e0f7901d8eba245", + [] + ], + "parse_handshake.py": [ + "af5a4ecae2c03353ba3e855bcdb5d74b0101466c", + [] + ], + "streams.py": [ + "ca24a598345e68a8d8e8c87d068d936fdf48dcbb", + [] + ] + } + }, + "fuzzing": { + "fuzz_http11_request_parser.py": [ + "59e0cea0f4393082668833c7a488eb87576a732d", + [] + ], + "fuzz_http11_response_parser.py": [ + "6906720a49d3a83b92b3386ebe57aed2f2e488bb", + [] + ], + "fuzz_websocket_parser.py": [ + "1509a3549d7b810d0da558f6b29ee9bfa1c77921", + [] + ] + }, + "logo": { + "favicon.ico": [ + "36e855029d705e72d44428bda6e8cb6d3dd317ed", + [] + ], + "github-social-preview.html": [ + "7f2b45badb481e4ea2a1ecf8461597e5c95b7672", + [] + ], + "github-social-preview.png": [ + "59a51b6e33810bc0bc34b7d2ea6e47b0fd8be78b", + [] + ], + "horizontal.svg": [ + "ee872dc47869fd63c9f22c1e8dad3a74f64797d2", + [] + ], + "icon.html": [ + "6a71ec23bcfd99ac96b9bee491a530810b7c8151", + [] + ], + "icon.svg": [ + "cb760940aa1928e6df4112abebe7a1d16f1a2778", + [] + ], + "old.svg": [ + "a073139e3311d3bc4e2997e15542cf1f3ddfccdf", + [] + ], + "tidelift.png": [ + "317dc4d9852df72ba34e10a6f61d1838cbbd969e", + [] + ], + "vertical.svg": [ + "b07fb22387385d41d6e1050e603641ea1fe54e12", + [] + ] + }, + "pyproject.toml": [ + "f24616dd7ea14aba7f07a18b09bd1cf78f5e94e0", [] ], "setup.py": [ - "b2d07737d227a0e45dbe87f9696116f1a10ed342", + "ae0aaa65de7a4e5447d83b1d0f0f86562892a789", [] ], "src": { "websockets": { "__init__.py": [ - "ec34841247b1b310461f588dc42a3a8cd0020c54", + "fdb028f4c4f0bb882fe8bcba623c163f7fc41732", [] ], "__main__.py": [ - "c562d21b54473acb2c41767d745f5ee0740b82dc", + "f2ea5cf4e8fe949948b1b6872d8e596ef30102ea", [] ], "auth.py": [ - "afcb38cffe2f48657a71bb4df47a3c9480055411", + "b792e02f5ccc99837eda66b124d0a3df374d0577", [] ], "client.py": [ - "df8e53429abaf6f4e5fb71772de860fc8896d7d7", + "b2f622042df8d4b560488cbbe217b42c7f135e44", [] ], "connection.py": [ - "db8b5369935e171d9f619d3e6336ac2e671630d9", + "88bcda1aafe7e59b336fcb0ef2519a609d581e0f", [] ], "datastructures.py": [ - "36a2cbaf99cee07fb08fd484681f902498c6e0ad", + "a0a648463adb0baf0fd5569c877171a266989d13", [] ], "exceptions.py": [ - "0c4fc51851a596d7d974a17ad3a2b220ade6bbb2", + "f7169e3b178ae7009fc32bb19590635847d5c625", [] ], "extensions": { @@ -476233,16 +477574,16 @@ [] ], "base.py": [ - "0609676185785d8ab3546824e430b8b4d47d7d66", + "6c481a46cc74cf8eb6a2d2551919f9d42ed304d8", [] ], "permessage_deflate.py": [ - "e0de5e8f855051a834399614089bbe6abb6c12a2", + "b391837c66686678cd1213b4c2b0de278bedc96b", [] ] }, "frames.py": [ - "043b688b5227b9e6dbd90115b528edb2bb62376a", + "6b1befb2e035ce18e20ab34b5358a98e90b2dc6e", [] ], "headers.py": [ @@ -476250,11 +477591,11 @@ [] ], "http.py": [ - "b14fa94bdce765dd6d46def2f3d24d98e39d329c", + "9f86f6a1ffa6b229aefdf4c65453d2c3d37fd2f6", [] ], "http11.py": [ - "84048fa47b3dccf93bb30e49ad88c384cbc6d9d1", + "ec4e3b8b7d5b84c9db160dbb725b83e457ad4424", [] ], "imports.py": [ @@ -476266,61 +477607,99 @@ "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", [] ], + "async_timeout.py": [ + "8264094f5b224b62ce5a4d14d4b6401a8d173f82", + [] + ], "auth.py": [ - "8825c14ecf7bf9e23b42d2c64955ae7768478902", + "d3425836e18b21b0daefc62793a90e97e1e1a6a8", [] ], "client.py": [ - "fadc3efe87667759f024d162dd1340aa27011e6b", + "48622523ee1f8685e707f6c10fdf06eabb6b2ef0", [] ], "compatibility.py": [ - "df81de9dbcead50479ec645c4625cadca01f3efc", + "6bd01e70dee10be4a7a517598bd2f2b95adb30c3", [] ], "framing.py": [ - "c4de7eb28bc1964a5177ba67f00b0ec2ec64f054", + "b77b869e3faad0518d1434d94761a9a85cd28c64", [] ], "handshake.py": [ - "569937bb9a94da186a9b0ef75b0a163540954518", + "ad8faf04045e7184bbff40bf7c17cbc1cb788f0c", [] ], "http.py": [ - "cc2ef1f067d39cddfe3a701f4e2e604641bdb3d6", + "2ac7f7092d58c8caf2b7289dc3ce334f167327b1", [] ], "protocol.py": [ - "3f734fe7602a12d499e87b14c7620fbe0652c9fa", + "19cee0e652b0cf9f8528718bb778acde35b8818a", [] ], "server.py": [ - "3e51db1b71e14506aac6adf1d8f129145ecfeaf1", + "7c24dd74af3eab3f6d1077b38d44e6afcc59326d", [] ] }, + "protocol.py": [ + "765e6b9bb4b72ac8b7e3f81d5264f4bcc4aac843", + [] + ], "py.typed": [ "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", [] ], "server.py": [ - "5dad50b6a12a61c9219a7678d38e733225deeb49", + "191660553ffd2a546adceda4df23a9cfa65aa2a6", [] ], "speedups.c": [ "a195904198f40078b7e772baafce610838b31db1", [] ], + "speedups.pyi": [ + "821438a064e6ad32154eb6536c975f70d4c35d05", + [] + ], "streams.py": [ "f861d4bd25ecc5b5a4ac5ebc22a251712fb4e68e", [] ], + "sync": { + "__init__.py": [ + "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + [] + ], + "client.py": [ + "087ff5f569a3705109b5bd92071f1422c920f8d5", + [] + ], + "connection.py": [ + "4a8879e3705a91013eb60ded201afef9af0a475d", + [] + ], + "messages.py": [ + "67a22313ca163da162cedabc29bc082a396727f7", + [] + ], + "server.py": [ + "14767968c97c7e203d1a7c6442cd390e9cd1f303", + [] + ], + "utils.py": [ + "471f32e19d4ebaf78938c18bd38792e253d2ac85", + [] + ] + }, "typing.py": [ - "e672ba0069e5b2c45c47994b4435313b6aea170f", + "cc3e3ec0d96f48f5918d755b454ffea86537aed1", [] ], "uri.py": [ - "fff0c380645ba6e25696ac45ef6dd59cc4d72f7f", + "385090f66ae36def8bca11e311803f2bec4ad558", [] ], "utils.py": [ @@ -476328,33 +477707,203 @@ [] ], "version.py": [ - "c30bfd68f31ab1be4ac213478cdd9cd6c0119b5b", + "d1c99458e2cf818d9104d9ecc58eccba2883fb8b", + [] + ] + } + }, + "tests": { + "__init__.py": [ + "dd78609f5ba12743a4f945799a22013e1d132e8a", + [] + ], + "extensions": { + "__init__.py": [ + "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + [] + ], + "test_base.py": [ + "b18ffb6fb8638b235ebb8364e294ce370e572e14", + [] + ], + "test_permessage_deflate.py": [ + "0e698566fb99a06bfa63de388d1115b0c9f8c8fe", + [] + ], + "utils.py": [ + "24fb74b4e6ede2843df9dccd858e03f2e3c0d9c1", [] ] }, - "websockets.egg-info": { - "PKG-INFO": [ - "3b042a3f9f822cace1abce4264b479420920fb48", + "legacy": { + "__init__.py": [ + "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + [] + ], + "test_auth.py": [ + "3754bcf3a5db7e8f397457465868dae78f7e2f3d", + [] + ], + "test_client_server.py": [ + "c49d91b707581a888ea5f11b3815420884b05e8e", + [] + ], + "test_framing.py": [ + "e1e4c891b03074ee67cc430d02accdd66986ddc5", [] ], - "SOURCES.txt": [ - "2a51106feebeea3637bfbbe4d830aadfb3ed2c20", + "test_handshake.py": [ + "661ae64fc47962d7a6d0428a23c193e7c73517df", [] ], - "dependency_links.txt": [ - "8b137891791fe96927ad78e64b0aad7bded08bdc", + "test_http.py": [ + "15d53e08d229cce6ef01076d14775e4158c89a2b", [] ], - "not-zip-safe": [ - "8b137891791fe96927ad78e64b0aad7bded08bdc", + "test_protocol.py": [ + "f2eb0fea03f790299175bb4e86f83f8cb7a44223", [] ], - "top_level.txt": [ - "5474af7431bb2a6c326a2627fb198ca3bfb9b439", + "utils.py": [ + "4a21dcaeb59679ee02fdb98ab7097c957dc80d66", [] ] - } - } + }, + "maxi_cov.py": [ + "2568dcf18bcace2f841ff144f1abee019e9abda1", + [] + ], + "protocol.py": [ + "4e843daab34f865b012ec41408c2804b6ddce9a5", + [] + ], + "sync": { + "__init__.py": [ + "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", + [] + ], + "client.py": [ + "683893e88c96c340d7837f1013a6c4ae2a90caa6", + [] + ], + "connection.py": [ + "89d4909ee1312cf520f7f617566f31f73462f992", + [] + ], + "server.py": [ + "a9a77438ca90df433d6563bf0c280f5ee332db15", + [] + ], + "test_client.py": [ + "c900f3b0fe6e07528dc3af80684fc82f2bd027e0", + [] + ], + "test_connection.py": [ + "63544d4add7c136f7ad8c4a3942f96f2eea51e89", + [] + ], + "test_messages.py": [ + "825eb879740c0d3774ffda7ddeb6987af04eba8f", + [] + ], + "test_server.py": [ + "f9db842468316a99efdf236f34c32e88ff1cd1c6", + [] + ], + "test_utils.py": [ + "2980a97b428f5f1d7fce99f37f13851358cc117a", + [] + ], + "utils.py": [ + "8903cd349923aa3049466cb60858f67121212376", + [] + ] + }, + "test_auth.py": [ + "28db931552190f99e3679ee94b7231a3c1020088", + [] + ], + "test_client.py": [ + "c83c87038f6ed921f00112b4e35cf4f96dd47d4d", + [] + ], + "test_connection.py": [ + "6592d67d0d4a5f1e8405ce28d389acca63717f10", + [] + ], + "test_datastructures.py": [ + "32b79817ae754f024a509f2f6d68c8e355b5b6c9", + [] + ], + "test_exceptions.py": [ + "1e6f58fad583a5dcdd91ddee2bd1bd82737cefe3", + [] + ], + "test_exports.py": [ + "67a1a6f994fb4ff5c1cb634aa72e3b83dce06da6", + [] + ], + "test_frames.py": [ + "e323b3b57c82371ce7c6064061b676d502d98714", + [] + ], + "test_headers.py": [ + "4ebd8b90cfefd004b70256c52c9744fa159efa8a", + [] + ], + "test_http.py": [ + "036bc14102b64d2eccb33810f79b4a419b3fb0aa", + [] + ], + "test_http11.py": [ + "d2e5e04627c2c45d33615849555e530bfe6de432", + [] + ], + "test_imports.py": [ + "b69ed931626e9858bd6ee4e0e8eccb0bdb60f31c", + [] + ], + "test_localhost.cnf": [ + "4069e39670c982bba4d221dd88ebd5910041c084", + [] + ], + "test_localhost.pem": [ + "8df63ec8f4b3aa3e0f0f274c317fec6bd16cd15b", + [] + ], + "test_protocol.py": [ + "a64172b539801f8215f27a62a8fffebabea5ef4b", + [] + ], + "test_server.py": [ + "b6f5e3568195e25b68258670c15570bd3f888aea", + [] + ], + "test_streams.py": [ + "fd7c66a0bdc95eac88148387db0573a5c90b4496", + [] + ], + "test_typing.py": [ + "202de840f359a890631d166912d3fcef1d7cc317", + [] + ], + "test_uri.py": [ + "8acc01c18716a27a33f9762fcea71d8260c2d5a0", + [] + ], + "test_utils.py": [ + "678fcfe798ec7c25c94680caa0995864b9c2f389", + [] + ], + "utils.py": [ + "2937a2f15e40a3b46c71d81949f8a81f11ac6d36", + [] + ] + }, + "tox.ini": [ + "939d8c0cd807d5273dab8340716648fd7d9cc977", + [] + ] }, "zipp": { ".flake8": [ @@ -477093,7 +478642,7 @@ [] ], "requirements.txt": [ - "b2c151b8fe96a025ad3f6d659fb40ad4053f2b34", + "3bb476fd968b2d754dcadb34b5331db6bf8b7712", [] ], "resources": { @@ -477552,7 +479101,7 @@ [] ], "capsule.py": [ - "74ca71ade9cce9702efd93ddc8b3e786d22563cb", + "fc8183a65f03a01a3127f342ce65120145d21d17", [] ], "handler.py": [ @@ -477611,11 +479160,11 @@ [] ], "requirements_android.txt": [ - "17672383cb895b74dc5679548ec1032e05dc8520", + "e8205caa70102c3a00f64a9bde680f5e95f94de4", [] ], "requirements_install.txt": [ - "55bed99f8c594a0cb290aba4df30bdadfdf324d7", + "f7df06548c9424a1039db27f9772845b217ef124", [] ], "revlist.py": [ @@ -477623,7 +479172,7 @@ [] ], "run.py": [ - "9d3c98c5215d5b9e80e03836cf9761bb16b33df2", + "2277bb244398a07f10741e621b28e107d3d410f9", [] ], "testfiles.py": [ @@ -477779,7 +479328,7 @@ ] }, "requirements.txt": [ - "93c17bf3bff2dfeb73fc0f419926d893b0dc72f3", + "356223ededc36642d3019d907c8d7703ff132667", [] ], "requirements_chromium.txt": [ @@ -477787,11 +479336,11 @@ [] ], "requirements_firefox.txt": [ - "3ba4731494acfef5fc9bd75ff3e76111575b2851", + "0ed5c0531665de5e4830ea174a1df88aea52c22d", [] ], "requirements_opera.txt": [ - "db0c5dd99234ed3a0f090af8b01ebfc14b311468", + "6c2425f337bf68153ca724cb5eaf4a6458e3de57", [] ], "requirements_safari.txt": [ @@ -477799,7 +479348,7 @@ [] ], "requirements_sauce.txt": [ - "c9e42346ce48d7353aef7cc10740a6cc0a35c884", + "806352e87e2471e760e2c2e5916353e2ec34dfa1", [] ], "setup.py": [ @@ -477829,7 +479378,7 @@ [] ], "chrome.py": [ - "dfac89cb5ecca44ac6b9fba8c41547cad2d3016b", + "95a51451a32eb35b60bfeabff09de2bfbde4cb8b", [] ], "chrome_android.py": [ @@ -477861,11 +479410,11 @@ [] ], "firefox.py": [ - "814b8b8d758464f320cdbff2d87bfa8ee821a64a", + "d977930a289b614caf4e0c598876500b20630497", [] ], "firefox_android.py": [ - "7c158902e1a3251f7ad7e51fb3ffadb35db21903", + "526f83d595f23d6fb5c30381bf885670498b807f", [] ], "ladybird.py": [ @@ -477929,7 +479478,7 @@ [] ], "base.py": [ - "763b6fcb19fb3298707073c0c4fa5b9ff758cf81", + "92a782e835c11b2394cc2be49f3f31a63ddaa0e7", [] ], "executorchrome.py": [ @@ -477957,7 +479506,7 @@ [] ], "executorwebdriver.py": [ - "3a2f29bdc9df206f09e3e8f284280be881a38e56", + "69013e5e796979133dafd5df08de347320244f43", [] ], "executorwktr.py": [ @@ -478304,7 +479853,7 @@ } }, "wptrunner.py": [ - "d65369b380d561a02e312e9418941b8a99c71ab9", + "d9d85de6a4d04b33d4ca231a44ce3cba7974a575", [] ], "wpttest.py": [ @@ -481308,7 +482857,7 @@ [] ], "video-encoder-utils.js": [ - "0838260d31bc8f90d2d294341f078bef544d29bd", + "916f995156e51d7fed083d9ab175cc44d974dc6b", [] ], "videoFrame-construction.crossOriginIsolated.https.any.js.headers": [ @@ -482340,7 +483889,7 @@ [] ], "fixtures_bidi.py": [ - "32919210bf2dac6a6bd58dc3a5af8241de682e16", + "c76b369f21c44bc071d8f045a9662aff1173ebd7", [] ], "fixtures_http.py": [ @@ -482856,7 +484405,7 @@ [] ], "prelu.json": [ - "cf79bee7a9d72ff233de9554240392219abe4d17", + "14a7c412dd77e184be76b39bd616891d91855376", [] ], "reciprocal.json": [ @@ -482932,7 +484481,7 @@ [] ], "softplus.json": [ - "eb05b7b281144cbbb2d0a5ec4748883b46a7ad03", + "373612d5ca23d664a517bdd536a71d4686b2f21c", [] ], "softsign.json": [ @@ -482973,7 +484522,7 @@ ] }, "utils.js": [ - "d1dc0675a7f5e75d3faca852e70b8a43d6722ced", + "653e036cd119d3198fd1c83be548b4999214f0f4", [] ], "utils_validation.js": [ @@ -486796,6 +488345,10 @@ "1e68302a0fc40c592be4a95387bfc315bd79d261", [] ], + "Worker-creation-happens-in-parallel.js.headers": [ + "8249c49c340f5e13902ff91deeacd79c3f148c45", + [] + ], "Worker-messageport.js": [ "1e01b0a52b586b00cbbb28b2352959f7fa150e39", [] @@ -486804,6 +488357,10 @@ "48b4c34fe3a6c55772802d23e570544af9314f68", [] ], + "Worker-postMessage-happens-in-parallel.js.headers": [ + "8249c49c340f5e13902ff91deeacd79c3f148c45", + [] + ], "Worker-run-forever-during-dynamic-import.js": [ "62840bd5b4ffad5e9b122a9971f271c1d9604abe", [] @@ -501765,7 +503322,7 @@ ] ], "comp_name_from_content.html": [ - "3504658ea46156ab7cac72fd7d2b92089c9ddc63", + "2f6b3ab6b474778ff4b296dafd40be90132be6a9", [ null, { @@ -501774,7 +503331,7 @@ ] ], "comp_text_node.html": [ - "a31f9e02451f72b9bb697f4820e637d56e464e4e", + "f9cb8f1baf837e10b55ca16275e48e47c8b69406", [ null, { @@ -511239,7 +512796,7 @@ }, "close-watcher": { "abortsignal.html": [ - "9229b37cf665d0db49d1a0b42349f00f5a2edcec", + "ec360f483d43a1ae5e02ec17d5c8b750a9211734", [ null, { @@ -511248,7 +512805,7 @@ ] ], "basic.html": [ - "9951e54031c794d19bb3a7742e99c063dda89bb8", + "79a91e127ef6846427d63ff0c21fe42dcb9611de", [ null, { @@ -511267,7 +512824,7 @@ ] ], "keypress.html": [ - "8dd58b064d7178184511a465e24b39220424abc2", + "c3bfcc0c3305cfd75f8f6de0572954967af20d24", [ null, { @@ -511276,7 +512833,7 @@ ] ], "keyup.html": [ - "341012d6bc8b54eef9956912e8ebf09769fe9982", + "7c75ef7969caffcde9cb74258a6d038a73281a70", [ null, { @@ -511285,7 +512842,7 @@ ] ], "not-user-activation.html": [ - "ac29f84f06cd25eea8a56ea59a346c25849d8721", + "a8d5d22fcf8ee3431c5a78ae19ad7f179e225140", [ null, { @@ -511320,7 +512877,7 @@ ] ], "inside-event-listeners.html": [ - "ac037fc147fa888f4e4bdf923c64076ba04c2922", + "47f431e25036748f2e5106549660f8c9bb8cf483", [ null, { @@ -511330,7 +512887,7 @@ ], "user-activation": { "n-activate-preventDefault.html": [ - "531ef425998cb3d88f82b29c5f875f68e8c86195", + "f413448718e9d1dcd05e37af780c341d7f923833", [ "close-watcher/user-activation/n-activate-preventDefault.html?CloseWatcher", { @@ -511345,7 +512902,7 @@ ] ], "n-activate.html": [ - "babcf54c3c160dd48a85919543b37fc0657b6741", + "d8253ba7656bb1ca69fe6c3e652007527eaa09ad", [ "close-watcher/user-activation/n-activate.html?CloseWatcher", { @@ -511360,7 +512917,7 @@ ] ], "n-closerequest-n.html": [ - "2424af7820e5ab442e314d7a82fbcddc60369913", + "54ccdd1abe2cac3d0ea60103f00007b4cfe36d15", [ "close-watcher/user-activation/n-closerequest-n.html?CloseWatcher", { @@ -511375,7 +512932,7 @@ ] ], "n-destroy-n.html": [ - "c26f87dd6f81352bdc6f678015d3645ef33f61e3", + "e0a94f490e887174d2822d16e9dd3bff02efcd1a", [ "close-watcher/user-activation/n-destroy-n.html?CloseWatcher", { @@ -511390,7 +512947,7 @@ ] ], "n.html": [ - "fe04e0dc1b9efb91a131d5150641367d41b4d3fa", + "af8f972ee61f482f9c56fd14471468dbc8d06829", [ "close-watcher/user-activation/n.html?CloseWatcher", { @@ -511404,8 +512961,8 @@ } ] ], - "nn-activate-CloseWatcher.html": [ - "8045f30b482647d9364876d812e3eeb5b0cb9116", + "nn-CloseWatcher.html": [ + "016745dfbb41fde8361addfe9be5aa0348270c8e", [ null, { @@ -511413,8 +512970,8 @@ } ] ], - "nn-activate-dialog.html": [ - "5cc866044ce26cd7b94fb8bb3b125d0e651eb276", + "nn-activate-CloseWatcher.html": [ + "45718e51a88cbafbd53ccf8e596c2dbaef532f42", [ null, { @@ -511422,23 +512979,26 @@ } ] ], - "nn.html": [ - "beb63f1b4f531cd68405e666480987c9a876c479", + "nn-activate-dialog.html": [ + "eaffb4d9a7bdb461cc52288d2649004a2b0ac3a7", [ - "close-watcher/user-activation/nn.html?CloseWatcher", + null, { "testdriver": true } - ], + ] + ], + "nn-dialog.html": [ + "0d086a525ef3c1c1d958a6889ba8ccb838e7eaad", [ - "close-watcher/user-activation/nn.html?dialog", + null, { "testdriver": true } ] ], "nnn-CloseWatcher-dialog-popover.html": [ - "f8b9061d019fae0191b4528978b1275a87efda03", + "38dd6073121db403b4b18ff7d500c841309cf23f", [ null, { @@ -511446,8 +513006,8 @@ } ] ], - "nnn-popovers.html": [ - "ed5d15598fcdd1f24880f133fcd0dc6d852f8add", + "nnn-CloseWatcher.html": [ + "5d2f07e6173d79ec7e2c8df5fe470d7cd05dfb01", [ null, { @@ -511455,23 +513015,26 @@ } ] ], - "nnn.html": [ - "9b604e91db2dd2f4967551c9aadd323125e4dc51", + "nnn-dialog.html": [ + "f1c071dbb3ec16f600fa68838b105a03c3493613", [ - "close-watcher/user-activation/nnn.html?CloseWatcher", + null, { "testdriver": true } - ], + ] + ], + "nnn-popovers.html": [ + "ed5d15598fcdd1f24880f133fcd0dc6d852f8add", [ - "close-watcher/user-activation/nnn.html?dialog", + null, { "testdriver": true } ] ], "ny-activate-preventDefault.html": [ - "5ffb64b1134b3e4741a05f1453a37bab77613e04", + "7cd1c2e5080a61a8a6a7e19ef716f14883acd89a", [ "close-watcher/user-activation/ny-activate-preventDefault.html?CloseWatcher", { @@ -511486,7 +513049,7 @@ ] ], "ny.html": [ - "226912233e21aeb6df69d652c53cd1fe22bacc98", + "49f50a123e9d448ab2bbc4d9728490945c85d1da", [ "close-watcher/user-activation/ny.html?CloseWatcher", { @@ -511510,7 +513073,7 @@ ] ], "nyn.html": [ - "ec5153c7674a1efd37e27eacbc15420ced861f13", + "b227d566d4c5d51c5517ed712f37fafaf8fbb11b", [ "close-watcher/user-activation/nyn.html?CloseWatcher", { @@ -511525,7 +513088,7 @@ ] ], "nynn-destroy.html": [ - "8519c8a2a94067b89482e7374425757dd6bee83d", + "fb04109994dc247e406fc5f3246f1a5dcbb80529", [ "close-watcher/user-activation/nynn-destroy.html?CloseWatcher", { @@ -511540,7 +513103,7 @@ ] ], "nynn.html": [ - "f6e74a0ba1104e065f2f1b90ca57af752a6abc06", + "ed9203db665ca56b2924d1456b815219f467c8fb", [ "close-watcher/user-activation/nynn.html?CloseWatcher", { @@ -511554,38 +513117,44 @@ } ] ], - "nyyn.html": [ - "f3987c1a2145055b0405fc5c5e592936a00826b1", + "nyyn-CloseWatcher.html": [ + "4f60ef3c4baf524791abf750775b33cf1cbc0fee", [ - "close-watcher/user-activation/nyyn.html?CloseWatcher", + null, { "testdriver": true } - ], + ] + ], + "nyyn-dialog.html": [ + "44926fd5c3e13a188df8c3c37c0a26af3ce67561", [ - "close-watcher/user-activation/nyyn.html?dialog", + null, { "testdriver": true } ] ], - "nyyyn.html": [ - "6cb8f3a4456925429c4ccfe2b4b06c108e8b2b92", + "nyyyn-CloseWatcher.html": [ + "e2565a82a309cbc987e5927a74267880fbca5c28", [ - "close-watcher/user-activation/nyyyn.html?CloseWatcher", + null, { "testdriver": true } - ], + ] + ], + "nyyyn-dialog.html": [ + "86361124d302f265bcc8aa9912f99c95973c7a2d", [ - "close-watcher/user-activation/nyyyn.html?dialog", + null, { "testdriver": true } ] ], "y.html": [ - "ee58a92293ab9831c7d8e6c93ed89595155f858d", + "78c432de3888eed80fcaf1cdd3d3c03e1a2b3257", [ "close-watcher/user-activation/y.html?CloseWatcher", { @@ -511600,7 +513169,7 @@ ] ], "yn-activate.html": [ - "af7289aa28e14f05a02fdcdde251f001ddb23699", + "d62b4df425098ab21b9ffbe622bb34660d856874", [ "close-watcher/user-activation/yn-activate.html?CloseWatcher", { @@ -511615,7 +513184,7 @@ ] ], "yn.html": [ - "8f7e90e2d85c8d847ec4e93a7252b39997caf5a1", + "578f43de25e0547afc50c2091d795a16da1d6be2", [ "close-watcher/user-activation/yn.html?CloseWatcher", { @@ -511629,23 +513198,26 @@ } ] ], - "ynn.html": [ - "8cc7f5bfb66c735780ee451c87f4dedb8173b33a", + "ynn-CloseWatcher.html": [ + "50b5a8131d11b385ad5a81b5a5f3757ebf5061b5", [ - "close-watcher/user-activation/ynn.html?CloseWatcher", + null, { "testdriver": true } - ], + ] + ], + "ynn-dialog.html": [ + "c10e94dc7350ea70e1f06f2bf9d1fa348c6f7f76", [ - "close-watcher/user-activation/ynn.html?dialog", + null, { "testdriver": true } ] ], "yy.html": [ - "0aa03cdd050ca8101714cc1b8e2c3fb2d240d281", + "9c0f21be22f2235223b7d005882a2bc21e0a6f5d", [ "close-watcher/user-activation/yy.html?CloseWatcher", { @@ -511660,7 +513232,7 @@ ] ], "yyn.html": [ - "b87cf7a7e342e7b9967bb1f429e95b79fb1a8fc7", + "2f753774446dc815974de7e29c401c8bc570813a", [ "close-watcher/user-activation/yyn.html?CloseWatcher", { @@ -511675,7 +513247,7 @@ ] ], "yyy-CloseWatcher-dialog-popover.html": [ - "f0a1cb06d101cd7da389b11bab1d7fe7108addf2", + "8650fb3b7c489b4554d010accf154b279a15feb5", [ null, { @@ -511684,7 +513256,7 @@ ] ], "yyy-activate-CloseWatcher-dialog-popover.html": [ - "ed41d1bc3210c1ddcb07c31ce4ff3a55cbd8cdd3", + "a58dd0751bb988670f05757489964266f55f4890", [ null, { @@ -511702,7 +513274,7 @@ ] ], "yyy.html": [ - "f16767a86bdfcf62b62d9ac69947fd93d084d1f4", + "eaf8944bcec8c188f2db5fd70aadafe72883e81c", [ "close-watcher/user-activation/yyy.html?CloseWatcher", { @@ -517831,6 +519403,13 @@ ] } ] + ], + "wildcard-host-part.sub.window.js": [ + "d210cc6670f66ce5b1c2c5b6534638a8cdb6997d", + [ + "content-security-policy/generic/wildcard-host-part.sub.window.html", + {} + ] ] }, "img-src": { @@ -523048,7 +524627,7 @@ ] ], "fedcm-context.https.html": [ - "7b3e1032af98b4be0b6b775e3ef39a26b1c7a0bf", + "f235437b789aee4dff2ecb639cd85f9c16f663a9", [ null, { @@ -523203,8 +524782,17 @@ ] ], "fedcm-multi-idp": { - "single-get-after-onload.https.html": [ - "de6a7c5371cc839a41c89318e5f8b0ad00b107e0", + "fedcm-multi-idp-abort.https.html": [ + "712a7b6a3494cbff372410f2517d8d9b14c5d49e", + [ + null, + { + "testdriver": true + } + ] + ], + "fedcm-multi-idp-basic.https.html": [ + "d855e0ad8dcf4eb86ce4bf3016337540bfafd45c", [ null, { @@ -523212,8 +524800,8 @@ } ] ], - "single-get-before-onload.https.html": [ - "0ac9b0e920245cfa81810f024190554d2248511f", + "fedcm-multi-idp-context.https.html": [ + "1bc3eb1f56295fef925c0d4bf0ea2aab46ed53ab", [ null, { @@ -523221,8 +524809,17 @@ } ] ], - "single-get-during-onload.https.html": [ - "832565744d4629aac869805fef10d135c82a9479", + "fedcm-multi-idp-mediation-optional.https.html": [ + "1a819efb314fa12eb0087a85682953f6a1705b29", + [ + null, + { + "testdriver": true + } + ] + ], + "fedcm-multi-idp-mediation-silent.https.html": [ + "d47d4898c7d72b1acde2e5486902ce5bdf966c93", [ null, { @@ -523316,7 +524913,7 @@ ], "fedcm-same-site-none": { "fedcm-same-site-none.https.html": [ - "77ecdaff9fe36e3bb4606798e9f69b4708cd2856", + "d3d20ea9df270cc23c48f7f709c9cc21bab7990e", [ null, { @@ -526876,27 +528473,6 @@ {} ] ], - "animation-delay-end-computed.tentative.html": [ - "77f96706383df2fb9a25478b5b35807b67bedf2d", - [ - null, - {} - ] - ], - "animation-delay-end-invalid.tentative.html": [ - "7cabd4e8e54d96d7c3b36fc9473022cf530f664d", - [ - null, - {} - ] - ], - "animation-delay-end-valid.tentative.html": [ - "162c781bb010066bfb17e87f64932180abb472c8", - [ - null, - {} - ] - ], "animation-delay-invalid.html": [ "1569497bb6a45225cb7f1f3c404512202d9da6e9", [ @@ -526904,41 +528480,6 @@ {} ] ], - "animation-delay-shorthand-computed.html": [ - "0a1eb96041c81ac14969d39a77013f8157495e5f", - [ - null, - {} - ] - ], - "animation-delay-shorthand.html": [ - "5c74a4d8e432ca41d02e5ec44330518f5162f457", - [ - null, - {} - ] - ], - "animation-delay-start-computed.tentative.html": [ - "bfb89d0267f5ad9a5ef7cb01e8ad6dc4b536a2fc", - [ - null, - {} - ] - ], - "animation-delay-start-invalid.tentative.html": [ - "bff31f3789a4fd9e8ff8be99e1af61882d46af62", - [ - null, - {} - ] - ], - "animation-delay-start-valid.tentative.html": [ - "f52286444edb71cdeb24bc57f8de8d566113730a", - [ - null, - {} - ] - ], "animation-delay-valid.html": [ "5ff0416cc64241f81025f39af20d186042d49a30", [ @@ -529321,7 +530862,7 @@ ] ], "all-prop-revert-layer.html": [ - "868267b285580466c8725f088f01cc3367f22b8b", + "3a1d621d1748ce32663a9eb91fad0162e35eedb7", [ null, {} @@ -531866,7 +533407,7 @@ "css-display": { "accessibility": { "display-contents-role-and-label.html": [ - "4a06d0ff5bf212648185c45f15c173c9437cfbc8", + "6b5453e9f64f049e34a7371b9ebab80fdbd6e7d4", [ null, { @@ -532077,7 +533618,7 @@ ] ], "linear-timing-functions-syntax.tentative.html": [ - "99b680d0bde07bbaa1824d7fcd13e78e0dc172f0", + "d2aa4f45b1a87c5b97bfc582cb3494095e5581c5", [ null, {} @@ -542214,6 +543755,24 @@ {} ] ], + "scroll-with-ancestor-border-radius.html": [ + "9192c613e9eb54bfc0ad774dcd429b18528c3b58", + [ + null, + { + "testdriver": true + } + ] + ], + "scroll-with-border-radius.html": [ + "88bb0f1fa88f7a9baaefcfcf9ab6c08a83836f95", + [ + null, + { + "testdriver": true + } + ] + ], "scrollable-overflow-float.html": [ "f75c0a66cfffdfe9872c6e472966ee3cf639eae6", [ @@ -544015,62 +545574,64 @@ {} ] ], - "highlight-cascade-007.html": [ - "de0322d91037848f821af8b3db194016069b5238", - [ - null, - {} - ] - ], - "highlight-cascade-009.html": [ - "acc48c7c3642cd267bf6e33c475d5b59c6d58fed", - [ - null, - {} - ] - ], - "highlight-currentcolor-computed-inheritance.html": [ - "d67ae828818aab04a61d7e55cbdfb23222277f93", - [ - null, - {} - ] - ], - "highlight-currentcolor-computed-visited.html": [ - "207cb7b7dd1301672d550f63b2b91827bd57c6f1", - [ - null, - {} - ] - ], - "highlight-currentcolor-computed.html": [ - "97c31809dfc76cd3fdbd051e66fd7ec162f3708e", - [ - null, - {} - ] - ], - "highlight-pseudos-computed.html": [ - "4a274e1bbd8506b7803f621f3f947c24be3780e3", - [ - null, - {} - ] - ], - "highlight-pseudos-inheritance-computed-001.html": [ - "84c4045a54c76f88e8ab59e8be4a0e601a580f2f", - [ - null, - {} - ] - ], - "highlight-pseudos-visited-computed-001.html": [ - "a2b18effccdcf78576da26003ea58985325f999b", - [ - null, - {} + "highlight-cascade": { + "highlight-cascade-007.html": [ + "df79d8dff51ae23a75a7a4ef593f673ea596f36d", + [ + null, + {} + ] + ], + "highlight-cascade-009.html": [ + "e95de9a78156c51ddd001b8a5b9a1dac3517da83", + [ + null, + {} + ] + ], + "highlight-currentcolor-computed-inheritance.html": [ + "d67ae828818aab04a61d7e55cbdfb23222277f93", + [ + null, + {} + ] + ], + "highlight-currentcolor-computed-visited.html": [ + "207cb7b7dd1301672d550f63b2b91827bd57c6f1", + [ + null, + {} + ] + ], + "highlight-currentcolor-computed.html": [ + "97c31809dfc76cd3fdbd051e66fd7ec162f3708e", + [ + null, + {} + ] + ], + "highlight-pseudos-computed.html": [ + "4a274e1bbd8506b7803f621f3f947c24be3780e3", + [ + null, + {} + ] + ], + "highlight-pseudos-inheritance-computed-001.html": [ + "84c4045a54c76f88e8ab59e8be4a0e601a580f2f", + [ + null, + {} + ] + ], + "highlight-pseudos-visited-computed-001.html": [ + "a2b18effccdcf78576da26003ea58985325f999b", + [ + null, + {} + ] ] - ], + }, "idlharness.html": [ "514d31f6b39ef066df52c113e68232119cb6927d", [ @@ -544282,7 +545843,7 @@ ] ], "line-spacing.html": [ - "9d3c6f23e7df297804470f048955e546feab67e7", + "4854e984c442c06028f929e63e1a5b25839efcf6", [ null, {} @@ -553199,6 +554760,13 @@ {} ] ], + "shadow-root-insertion.html": [ + "47fc665aa398458fe5df9cab4883284c9652387a", + [ + null, + {} + ] + ], "starting-of-transitions-001.html": [ "f80d8134366f861a7220acc348c69c4715de821e", [ @@ -554210,20 +555778,6 @@ {} ] ], - "animation-delay-end.tentative.html": [ - "79b9f946174eaf5dcbd39556f7010d6d74ea96c9", - [ - null, - {} - ] - ], - "animation-delay-start.tentative.html": [ - "2fba4d8e514471d540c17a384ab6803a4142155a", - [ - null, - {} - ] - ], "animation-delay.html": [ "91ee95ec1d5b9dcfb19fea16ea58a2caedb4d046", [ @@ -556722,7 +558276,7 @@ "calc-size": { "animation": { "calc-size-height-interpolation.tentative.html": [ - "06277376e9f266ca5b43890cafcda1c6c6b425fe", + "6d15c3f22654f7836934c830b46475ac1f07bd2e", [ null, { @@ -556748,7 +558302,7 @@ ] ], "calc-size-parsing.tentative.html": [ - "afcb200424fb13a75b71ad60dac835d245add719", + "422ab3c33ee1bd09fb7c02cf312c76bf4d19d8ba", [ null, {} @@ -556833,7 +558387,7 @@ ] ], "container-progress-computed.tentative.html": [ - "9ab537cad6c86c8abea1666a50ba81f9589cd88e", + "5c8d12f9cd492a530ea171af3434515b2f2a83ac", [ null, {} @@ -558100,7 +559654,7 @@ ] }, "scroll-top-test-with-zoom.html": [ - "290fa602af677f6fb394c3b79a9bb25c2158d23d", + "9656fe120e8ec39a7570950b4b1ad39d6c2fb16b", [ null, {} @@ -563672,7 +565226,16 @@ ] ], "user-invalid.html": [ - "5f8fa5811aff1b0cf333f5dc5bd173c6507b8810", + "c57e764831290886de7651735bda731ed4819e35", + [ + null, + { + "testdriver": true + } + ] + ], + "user-valid-user-invalid-multifield-inputs.tentative.html": [ + "d6358ea2691173da800ceb275a13406638291741", [ null, { @@ -563681,7 +565244,7 @@ ] ], "user-valid.html": [ - "7a12eb237d34af1645730435f44509a9af882677", + "402003ba5e63266071a9c3aa474f017ddb131049", [ null, { @@ -564931,6 +566494,42 @@ ] }, "device-posture": { + "device-posture-change-event.https.html": [ + "eb2fc2d96f81eddceb14810d79d793dd44ddff7d", + [ + null, + { + "testdriver": true + } + ] + ], + "device-posture-clear.https.html": [ + "319cd7266af066a9e7762b44e92ba695ef06410c", + [ + null, + { + "testdriver": true + } + ] + ], + "device-posture-event-listener.https.html": [ + "f4e21c89ccd8876cb102539f88d706353cfe50f3", + [ + null, + { + "testdriver": true + } + ] + ], + "device-posture-media-queries.https.html": [ + "e4dbd2e7d81d3c3fc088283adc6b884ed4109018", + [ + null, + { + "testdriver": true + } + ] + ], "idlharness.https.window.js": [ "74ebd11af97835a9973186ff9a61131550bf54c3", [ @@ -568761,6 +570360,92 @@ null, {} ] + ], + "continue-css-animation-left.html": [ + "8c7f73e3c933ce6f6a9749281707c62e0a090d7d", + [ + null, + {} + ] + ], + "continue-css-animation-transform.html": [ + "e7a285893aa0e4a538ec29af944f1d0761d93f0a", + [ + null, + {} + ] + ], + "continue-css-transition-left-pseudo.html": [ + "fa51b168879f4ef12f7521a9a4e1a1492d340815", + [ + null, + {} + ] + ], + "continue-css-transition-left.html": [ + "2b8e04b26e469b38bb8d7c3750be9b54025f3060", + [ + null, + {} + ] + ], + "continue-css-transition-transform-pseudo.html": [ + "d02c72561c1101ce9474cc60b9295e46c9e499df", + [ + null, + {} + ] + ], + "continue-css-transition-transform.html": [ + "f09edca144978948e490eeccbc5a0cb915c8ab58", + [ + null, + {} + ] + ], + "css-animation-commit-styles.html": [ + "86bb7c33e4261ec04740824775249fdd7cbdc9b2", + [ + null, + {} + ] + ], + "css-transition-cross-document.html": [ + "f3c8fafbfa8c5b002ba309e3333a57536473d266", + [ + null, + {} + ] + ], + "css-transition-cross-shadow.html": [ + "145f40ba50469364ca11ea23963ad97070b4f39a", + [ + null, + {} + ] + ], + "css-transition-to-disconnected-document.html": [ + "537edfe9b617029c7a5780a84fd40078eea0748b", + [ + null, + {} + ] + ], + "css-transition-trigger.html": [ + "0cb5608a695e000dc54b9fa2f73c49fc16ebcd98", + [ + null, + {} + ] + ], + "fullscreen-preserve.html": [ + "810eeac9af90f80e18ed149e875b3656f81b3a1d", + [ + null, + { + "testdriver": true + } + ] ] } }, @@ -568863,8 +570548,19 @@ {} ] ], + "observable-every.any.js": [ + "74a344b8f78559ad9d23247a4d2dc87cf095320d", + [ + "dom/observable/tentative/observable-every.any.html", + {} + ], + [ + "dom/observable/tentative/observable-every.any.worker.html", + {} + ] + ], "observable-filter.any.js": [ - "8a49bcf4674624751c36924372bd8867d211a2aa", + "3c1a7d782488c8d7d2cfbfb0da0b26fc41bb70b0", [ "dom/observable/tentative/observable-filter.any.html", {} @@ -568874,6 +570570,17 @@ {} ] ], + "observable-find.any.js": [ + "0e09060fc5ac199f976f952b50e048546627b850", + [ + "dom/observable/tentative/observable-find.any.html", + {} + ], + [ + "dom/observable/tentative/observable-find.any.worker.html", + {} + ] + ], "observable-first.any.js": [ "7c99066dc22a82a9043349934b28f939187b3d39", [ @@ -568925,6 +570632,17 @@ {} ] ], + "observable-inspect.any.js": [ + "8aff741d2670db26a7a355fdd747e70082479a43", + [ + "dom/observable/tentative/observable-inspect.any.html", + {} + ], + [ + "dom/observable/tentative/observable-inspect.any.worker.html", + {} + ] + ], "observable-last.any.js": [ "cd39a3700a211aad77e673a2e096ecdb2bb8c8e9", [ @@ -568954,6 +570672,17 @@ {} ] ], + "observable-some.any.js": [ + "b692610df329c601d38b3407eac1b242b62f9b05", + [ + "dom/observable/tentative/observable-some.any.html", + {} + ], + [ + "dom/observable/tentative/observable-some.any.worker.html", + {} + ] + ], "observable-switchMap.any.js": [ "836a39a68e00f15c7e42a80354feb9ad7842c22d", [ @@ -570137,6 +571866,65 @@ } ] ], + "delete-without-unwrapping-first-line-of-child-block.html": [ + "99f8f058888d00428d6225af609e0565451d87c8", + [ + "editing/other/delete-without-unwrapping-first-line-of-child-block.html?method=BackspaceKey&lineBreak=br", + { + "testdriver": true, + "timeout": "long" + } + ], + [ + "editing/other/delete-without-unwrapping-first-line-of-child-block.html?method=BackspaceKey&lineBreak=preformat", + { + "testdriver": true, + "timeout": "long" + } + ], + [ + "editing/other/delete-without-unwrapping-first-line-of-child-block.html?method=DeleteKey&lineBreak=br", + { + "testdriver": true, + "timeout": "long" + } + ], + [ + "editing/other/delete-without-unwrapping-first-line-of-child-block.html?method=DeleteKey&lineBreak=preformat", + { + "testdriver": true, + "timeout": "long" + } + ], + [ + "editing/other/delete-without-unwrapping-first-line-of-child-block.html?method=deleteCommand&lineBreak=br", + { + "testdriver": true, + "timeout": "long" + } + ], + [ + "editing/other/delete-without-unwrapping-first-line-of-child-block.html?method=deleteCommand&lineBreak=preformat", + { + "testdriver": true, + "timeout": "long" + } + ], + [ + "editing/other/delete-without-unwrapping-first-line-of-child-block.html?method=forwardDeleteCommand&lineBreak=br", + { + "testdriver": true, + "timeout": "long" + } + ], + [ + "editing/other/delete-without-unwrapping-first-line-of-child-block.html?method=forwardDeleteCommand&lineBreak=preformat", + { + "testdriver": true, + "timeout": "long" + } + ] + ], "delete.html": [ "b9bd1437e3721f1353a9df6bde155f2f2b8ae95a", [ @@ -593012,6 +594800,55 @@ ] ] }, + "crashtests": { + "huge-fetch.any.js": [ + "1b09925d855f3f514f922782e474dcb5a596a37a", + [ + "fetch/api/crashtests/huge-fetch.any.html", + { + "script_metadata": [ + [ + "global", + "window,worker" + ] + ] + } + ], + [ + "fetch/api/crashtests/huge-fetch.any.serviceworker.html", + { + "script_metadata": [ + [ + "global", + "window,worker" + ] + ] + } + ], + [ + "fetch/api/crashtests/huge-fetch.any.sharedworker.html", + { + "script_metadata": [ + [ + "global", + "window,worker" + ] + ] + } + ], + [ + "fetch/api/crashtests/huge-fetch.any.worker.html", + { + "script_metadata": [ + [ + "global", + "window,worker" + ] + ] + } + ] + ] + }, "credentials": { "authentication-basic.any.js": [ "31ccc3869775fe0be5b5e403fdc3304054b399a5", @@ -595084,7 +596921,7 @@ ] }, "request-bad-port.any.js": [ - "5c29823eaa444e95a28bbad4e48c938b98bf61c9", + "915063bab56a19fd09d02a450fbf9b15774437c3", [ "fetch/api/request/request-bad-port.any.html", { @@ -601201,13 +603038,6 @@ ] ], "generated": { - "appcache-manifest.https.sub.html": [ - "cf322fd34bc9abfc9d0b96e8eb9308441c2eeb13", - [ - null, - {} - ] - ], "audioworklet.https.sub.html": [ "64fb7607e26a17969e10ce0308ed8f9bff60cb70", [ @@ -601672,7 +603502,7 @@ ] ], "worker-dedicated-constructor.sub.html": [ - "69ac7682a5c5ffc6699a51b5589a42ac2c08bedd", + "65b1837c6363a249bf0f249eed726432cd1bcca6", [ null, {} @@ -616734,7 +618564,7 @@ ] ], "disabled-by-permissions-policy.https.sub.html": [ - "f374a58f785c556b3f275dbf6c2eacb8d7a2d1a0", + "d3d94e1d3d74ba121ca169e9cb6689382f23f018", [ null, { @@ -616743,7 +618573,7 @@ ] ], "enabled-by-permission-policy-attribute-redirect-on-load.https.sub.html": [ - "c2a2b09fc65a959a02a5fa5e8eccc2c77ae9dd77", + "d25afa52bb7146b30f70792737f247879dd68690", [ null, {} @@ -616757,7 +618587,7 @@ ] ], "enabled-by-permissions-policy.https.sub.html": [ - "d5f72eb1ccb03d6230ef91b704479814610333a9", + "332e4cea16b22f94fab3c4e9b390fd4e119e04dd", [ null, { @@ -616766,7 +618596,7 @@ ] ], "enabled-on-self-origin-by-permissions-policy.https.sub.html": [ - "ea4b65966f1b870f4d4385225ed4ceaed831e9b1", + "5940888b3562a01ede345b39d5da1fecde79d954", [ null, { @@ -627484,29 +629314,8 @@ {} ] ], - "2d.fillStyle.parse.hsl-clamp-1.html": [ - "59d66c383c90d6a4c01ca327dd316aa4efae5608", - [ - null, - {} - ] - ], - "2d.fillStyle.parse.hsl-clamp-2.html": [ - "1a1939e47af3373d083a99f2d406df360c86fe8a", - [ - null, - {} - ] - ], - "2d.fillStyle.parse.hsl-clamp-3.html": [ - "56f3a0a8b540b6873f0eaeee4059b0fc71ee2025", - [ - null, - {} - ] - ], - "2d.fillStyle.parse.hsl-clamp-4.html": [ - "af9d11e678c7c5a0582c64627e62ad2e8af753ab", + "2d.fillStyle.parse.hsl-clamp-negative-saturation.html": [ + "65440c6228e67dff9a7572096ed532f0ea05644f", [ null, {} @@ -627526,43 +629335,22 @@ {} ] ], - "2d.fillStyle.parse.hsla-clamp-1.html": [ - "2acac26e1a97cdaf49a99ee795be7ff437d89977", + "2d.fillStyle.parse.hsla-clamp-alpha-1.html": [ + "e5dc98d4e3a90e698407cee2954e6de004e8b58b", [ null, {} ] ], - "2d.fillStyle.parse.hsla-clamp-2.html": [ - "0f32fb5474a517c6f527d5cd217b0085d4829bb4", + "2d.fillStyle.parse.hsla-clamp-alpha-2.html": [ + "26139a562ee57d63eca44d3c5348e05569f75fbe", [ null, {} ] ], - "2d.fillStyle.parse.hsla-clamp-3.html": [ - "4bc134aec5ceba79cc4991cdac16ba2a4c71c624", - [ - null, - {} - ] - ], - "2d.fillStyle.parse.hsla-clamp-4.html": [ - "f8b2382755a7c51d33f34798328389aa9776a03e", - [ - null, - {} - ] - ], - "2d.fillStyle.parse.hsla-clamp-5.html": [ - "9c5e2258b9e5a108daa2729969d4621262f4a778", - [ - null, - {} - ] - ], - "2d.fillStyle.parse.hsla-clamp-6.html": [ - "153515eeddacbeaf784415ea9a20260795cbec54", + "2d.fillStyle.parse.hsla-clamp-negative-saturation.html": [ + "2d9b9d3bdf9aec50271993fca60c5ffe25b813a7", [ null, {} @@ -636838,6 +638626,20 @@ {} ] ], + "2d.fillStyle.parse.hsl-clamp-negative-saturation.html": [ + "69277677b96c28a23249f932e5054ec56059dcd8", + [ + null, + {} + ] + ], + "2d.fillStyle.parse.hsl-clamp-negative-saturation.worker.js": [ + "7f1e91ddb7b81e42ff0e0d7a74e53f8c777f41b5", + [ + "html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.worker.html", + {} + ] + ], "2d.fillStyle.parse.hsla-1.html": [ "67a65da730e9c7c66dec30eef1bf23a28fa2e6ce", [ @@ -636950,6 +638752,48 @@ {} ] ], + "2d.fillStyle.parse.hsla-clamp-alpha-1.html": [ + "5d73d34c9c64b40348dcca200359b6f6cb9ccd4d", + [ + null, + {} + ] + ], + "2d.fillStyle.parse.hsla-clamp-alpha-1.worker.js": [ + "7acb76d80b83aa01f59903006d8b6f557f30d582", + [ + "html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.worker.html", + {} + ] + ], + "2d.fillStyle.parse.hsla-clamp-alpha-2.html": [ + "eaf7a6af89cf0cb04641cd29c99740d3d00f7930", + [ + null, + {} + ] + ], + "2d.fillStyle.parse.hsla-clamp-alpha-2.worker.js": [ + "540b3ea15f08e6dfb2b956cf9ad05a447d687d27", + [ + "html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.worker.html", + {} + ] + ], + "2d.fillStyle.parse.hsla-clamp-negative-saturation.html": [ + "04749fb4a79bbd2cf1bb54ccec985ee77aa3d301", + [ + null, + {} + ] + ], + "2d.fillStyle.parse.hsla-clamp-negative-saturation.worker.js": [ + "f5fe6d4296dca10d062b76b5d3ba2f9d2af387f0", + [ + "html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.worker.html", + {} + ] + ], "2d.fillStyle.parse.html4.html": [ "fc1c1af18b4938f6375fe9e5c16f814bd6cc346a", [ @@ -650550,7 +652394,7 @@ ] ], "aria-element-reflection.html": [ - "bdf2450708bfb31fde393a08d99bef38a1e68ad1", + "e04610171b5ae20d827d6370c2005c6366d99698", [ null, {} @@ -651933,7 +653777,7 @@ ] ], "usvstring-reflection.https.html": [ - "775cb49281eb4565a5b2109e5e94e4cc9834f500", + "d8d830dc59714555811fc9e49dbece35d084a8ab", [ null, {} @@ -662091,6 +663935,15 @@ {} ] ], + "form-validation-validity-textarea-defaultValue.html": [ + "55276116adea957ea082242071c4d47808b4bf7d", + [ + null, + { + "testdriver": true + } + ] + ], "form-validation-validity-tooLong.html": [ "aa787d471d2c50cd1102ddade71d628a33f4f23e", [ @@ -663327,6 +665180,13 @@ {} ] ], + "input-stepdown-02.html": [ + "db71d110092849c90f2f96a5ea476b5476dc8913", + [ + null, + {} + ] + ], "input-stepdown-weekmonth.html": [ "c50f67fce53cac40390ff24f4a15d22b3daf4a55", [ @@ -665774,7 +667634,7 @@ ] ], "bounded-sizes.tentative.html": [ - "405e2364f54be492033d350987d238b1b0be1b6e", + "2010cd0a544565493d6e443e873f81c360de653e", [ null, {} @@ -678513,7 +680373,7 @@ ] ], "input-events-get-target-ranges-deleting-in-list-items.tentative.html": [ - "7c225cd051d59743ab45f7674a0e945fbc6a33ae", + "3572d227f00e7a607d9333e316adae1fca26587c", [ "input-events/input-events-get-target-ranges-deleting-in-list-items.tentative.html?Backspace,ol", { @@ -682365,7 +684225,7 @@ ] ], "loaf-stream-source-location.html": [ - "5776ff5255243609132aa046e1ede5a3f8de1720", + "0fd30859d738dec410b9b8d20c0c9ba8c2f61294", [ null, { @@ -682374,7 +684234,7 @@ ] ], "loaf-stream.html": [ - "424f2cd0d1e629ab89df5c29a206b49b2d2fff51", + "e35bc2f9aa7d7de7a6ac224b9befde30d128e85c", [ null, { @@ -685564,7 +687424,7 @@ ] ], "MediaRecorder-canvas-media-source.https.html": [ - "0680c218795b1e217376177bea6f14b0e1a11a40", + "e640714d5cf38d1250c7b07a476a360aee0e81b8", [ "mediacapture-record/MediaRecorder-canvas-media-source.https.html?mimeType=''", { @@ -685573,13 +687433,6 @@ } ], [ - "mediacapture-record/MediaRecorder-canvas-media-source.https.html?mimeType=video/mp4", - { - "testdriver": true, - "timeout": "long" - } - ], - [ "mediacapture-record/MediaRecorder-canvas-media-source.https.html?mimeType=video/mp4;codecs=avc1,mp4a.40.2", { "testdriver": true, @@ -685587,13 +687440,6 @@ } ], [ - "mediacapture-record/MediaRecorder-canvas-media-source.https.html?mimeType=video/mp4;codecs=vp9,opus", - { - "testdriver": true, - "timeout": "long" - } - ], - [ "mediacapture-record/MediaRecorder-canvas-media-source.https.html?mimeType=video/webm;codecs=av1,opus", { "testdriver": true, @@ -685655,7 +687501,7 @@ ] ], "MediaRecorder-events-and-exceptions.html": [ - "97ada2126634e550c8df7bd15807db0befa99a3f", + "409e46c91da21805e238c4237be481385a25e79f", [ "mediacapture-record/MediaRecorder-events-and-exceptions.html?mimeType=''", { @@ -685663,24 +687509,12 @@ } ], [ - "mediacapture-record/MediaRecorder-events-and-exceptions.html?mimeType=video/mp4", - { - "timeout": "long" - } - ], - [ "mediacapture-record/MediaRecorder-events-and-exceptions.html?mimeType=video/mp4;codecs=avc1,mp4a.40.2", { "timeout": "long" } ], [ - "mediacapture-record/MediaRecorder-events-and-exceptions.html?mimeType=video/mp4;codecs=vp9,opus", - { - "timeout": "long" - } - ], - [ "mediacapture-record/MediaRecorder-events-and-exceptions.html?mimeType=video/webm;codecs=av1,opus", { "timeout": "long" @@ -685700,14 +687534,14 @@ ] ], "MediaRecorder-mimetype.html": [ - "57baa1346f172ec766256969b17f8e44197d856f", + "74248d65f4660a25742d7baa5d050e40ed4199bd", [ null, {} ] ], "MediaRecorder-pause-resume.html": [ - "f584508a0d0d42d60ae332a42b3fe776bd3247a6", + "8dc231279a0233b19ef6b87b4318d1a98e23fb69", [ "mediacapture-record/MediaRecorder-pause-resume.html?mimeType=''", { @@ -685715,36 +687549,12 @@ } ], [ - "mediacapture-record/MediaRecorder-pause-resume.html?mimeType=video/mp4", - { - "timeout": "long" - } - ], - [ "mediacapture-record/MediaRecorder-pause-resume.html?mimeType=video/mp4;codecs=avc1,mp4a.40.2", { "timeout": "long" } ], [ - "mediacapture-record/MediaRecorder-pause-resume.html?mimeType=video/mp4;codecs=avc1,opus", - { - "timeout": "long" - } - ], - [ - "mediacapture-record/MediaRecorder-pause-resume.html?mimeType=video/mp4;codecs=vp9,mp4a.40.2", - { - "timeout": "long" - } - ], - [ - "mediacapture-record/MediaRecorder-pause-resume.html?mimeType=video/mp4;codecs=vp9,opus", - { - "timeout": "long" - } - ], - [ "mediacapture-record/MediaRecorder-pause-resume.html?mimeType=video/webm;codecs=av1,opus", { "timeout": "long" @@ -685774,93 +687584,9 @@ ] ], "MediaRecorder-peerconnection.https.html": [ - "daae044fa8d9d7d1f0fe40026ac9a42e96e129b9", - [ - "mediacapture-record/MediaRecorder-peerconnection.https.html?kinds=audio&mimeType=''", - { - "testdriver": true, - "timeout": "long" - } - ], - [ - "mediacapture-record/MediaRecorder-peerconnection.https.html?kinds=audio&mimeType=audio/mp4;codecs=opus", - { - "testdriver": true, - "timeout": "long" - } - ], - [ - "mediacapture-record/MediaRecorder-peerconnection.https.html?kinds=audio&mimeType=audio/webm;codecs=opus", - { - "testdriver": true, - "timeout": "long" - } - ], - [ - "mediacapture-record/MediaRecorder-peerconnection.https.html?kinds=video&mimeType=''", - { - "testdriver": true, - "timeout": "long" - } - ], - [ - "mediacapture-record/MediaRecorder-peerconnection.https.html?kinds=video&mimeType=video/mp4;codecs=vp9", - { - "testdriver": true, - "timeout": "long" - } - ], - [ - "mediacapture-record/MediaRecorder-peerconnection.https.html?kinds=video&mimeType=video/webm;codecs=vp8", - { - "testdriver": true, - "timeout": "long" - } - ], - [ - "mediacapture-record/MediaRecorder-peerconnection.https.html?kinds=video&mimeType=video/webm;codecs=vp9", - { - "testdriver": true, - "timeout": "long" - } - ], - [ - "mediacapture-record/MediaRecorder-peerconnection.https.html?kinds=video,audio&mimeType=''", - { - "testdriver": true, - "timeout": "long" - } - ], - [ - "mediacapture-record/MediaRecorder-peerconnection.https.html?kinds=video,audio&mimeType=video/mp4", - { - "testdriver": true, - "timeout": "long" - } - ], - [ - "mediacapture-record/MediaRecorder-peerconnection.https.html?kinds=video,audio&mimeType=video/mp4;codecs=avc1,mp4a.40.2", - { - "testdriver": true, - "timeout": "long" - } - ], + "3fbc1f0f2d292e4fd5cd41d751c0994fb7d658ed", [ - "mediacapture-record/MediaRecorder-peerconnection.https.html?kinds=video,audio&mimeType=video/mp4;codecs=vp9,opus", - { - "testdriver": true, - "timeout": "long" - } - ], - [ - "mediacapture-record/MediaRecorder-peerconnection.https.html?kinds=video,audio&mimeType=video/webm;codecs=vp8,opus", - { - "testdriver": true, - "timeout": "long" - } - ], - [ - "mediacapture-record/MediaRecorder-peerconnection.https.html?kinds=video,audio&mimeType=video/webm;codecs=vp9,opus", + null, { "testdriver": true, "timeout": "long" @@ -685875,24 +687601,16 @@ ] ], "MediaRecorder-stop.html": [ - "9ef5051638130f92f62753fb3aedf857bc8cbeba", + "d6ce3707720a066b06fef71016e3e2b0366d3658", [ "mediacapture-record/MediaRecorder-stop.html?mimeType=''", {} ], [ - "mediacapture-record/MediaRecorder-stop.html?mimeType=video/mp4", - {} - ], - [ "mediacapture-record/MediaRecorder-stop.html?mimeType=video/mp4;codecs=avc1,mp4a.40.2", {} ], [ - "mediacapture-record/MediaRecorder-stop.html?mimeType=video/mp4;codecs=vp9,opus", - {} - ], - [ "mediacapture-record/MediaRecorder-stop.html?mimeType=video/webm;codecs=av1,opus", {} ], @@ -696018,7 +697736,7 @@ }, "deviceproperties": { "get-device-properties-uniqueid-from-pointer-event.tentative.html": [ - "53b4d2c8d55a3895d7a8dd5ef1ed517a6fa99c2d", + "dc6b9379c11403baab12d5ac65032bdd766b1643", [ null, { @@ -696027,7 +697745,7 @@ ] ], "pointer-event-has-device-properties-uniqueid-from-pointer-event-init.tentative.html": [ - "029edef76944a10e462ad8418b3dc757c071e5a5", + "a37df4b4214501d4f7e5e7803ecfd3672c91b591", [ null, {} @@ -714431,7 +716149,7 @@ ] ], "animation-shorthand.html": [ - "b7d5947a212b67a5e9a0ce1b53785b562e622554", + "cb63137f5c953503709675507df2662f085e54cf", [ null, {} @@ -720557,7 +722275,65 @@ "testdriver": true } ] - ] + ], + "reading-order": { + "tentative": { + "grid-order-on-shadow-host.html": [ + "a48eda1b57cd26b642edeed2d890bbf01a6043fe", + [ + null, + { + "testdriver": true + } + ] + ], + "grid-order-with-iframe.html": [ + "87b9e0a83dfec85132859156bc54aead6914cae4", + [ + null, + { + "testdriver": true + } + ] + ], + "grid-order-with-nested-grids.html": [ + "d3da6682a3488e65bf2979aea344f5e43a956a49", + [ + null, + { + "testdriver": true + } + ] + ], + "grid-order-with-popover.html": [ + "2e98c041570d935f89dd753e04460ab098d7dd89", + [ + null, + { + "testdriver": true + } + ] + ], + "grid-order-with-slots.html": [ + "3045001e1d82525cbbf2e4524d86c28e4f208036", + [ + null, + { + "testdriver": true + } + ] + ], + "grid-order.html": [ + "1b86ab0b25a1d0e92aeae09b6f55b17bb97f1ee7", + [ + null, + { + "testdriver": true + } + ] + ] + } + } }, "focus-within-shadow.html": [ "c0a1e624fb79879f7601cba0da5854ae26988a00", @@ -721506,29 +723282,36 @@ {} ] ], - "cross-origin-create-worklet-failure-false-shared-storage-cross-origin-worklet-allowed.tentative.https.sub.html": [ - "db361776f638731401883e8605c5e4f9d2a28411", + "cross-origin-create-worklet-unrevealed-failure-false-shared-storage-cross-origin-worklet-allowed.tentative.https.sub.html": [ + "f1f37b0affd66f4cdc736ada641947f4e6c9471b", [ null, {} ] ], - "cross-origin-create-worklet-failure-missing-access-control-allow-credentials.tentative.https.sub.html": [ - "8887aad64dc72e5e36fc254c8a6b4221d4f0ef6b", + "cross-origin-create-worklet-unrevealed-failure-missing-access-control-allow-credentials.tentative.https.sub.html": [ + "dd6347e463171db65de24a9b57d6f1a754b78331", [ null, {} ] ], - "cross-origin-create-worklet-failure-missing-access-control-allow-origin.tentative.https.sub.html": [ - "58a2f3a77bbfb402f425be9c8a7262a9e65a3df4", + "cross-origin-create-worklet-unrevealed-failure-missing-access-control-allow-origin.tentative.https.sub.html": [ + "1f3223a5644372f1f76bcbe29bd8d064d7b41dce", [ null, {} ] ], - "cross-origin-create-worklet-failure-missing-shared-storage-cross-origin-worklet-allowed.tentative.https.sub.html": [ - "5b140a8141c96473af0ba4e5135bd5099094359d", + "cross-origin-create-worklet-unrevealed-failure-missing-shared-storage-cross-origin-worklet-allowed.tentative.https.sub.html": [ + "f96e4d596e8df094c5dc30eac67c609205d0fa27", + [ + null, + {} + ] + ], + "cross-origin-worklet-select-url-and-verify-data-origin.tentative.https.sub.html": [ + "5b6b9d5f8f5b5e04f89a651eb88e344dae8e957b", [ null, {} @@ -728379,7 +730162,7 @@ }, "readable-streams": { "async-iterator.any.js": [ - "4b674bea8430f3bc4ab8539c6671f37ba498d75d", + "e192201b531da7c24e0bcae1834f08a00cf8757d", [ "streams/readable-streams/async-iterator.any.html", { @@ -736112,8 +737895,8 @@ {} ] ], - "TrustedTypePolicyFactory-getPropertyType.html": [ - "a1039487a2433810ed274ade42ef124c6e820977", + "TrustedTypePolicyFactory-getPropertyType.tentative.html": [ + "e7218e9333a213df8643b628fc8dae294ef47ee0", [ null, {} @@ -736126,8 +737909,8 @@ {} ] ], - "TrustedTypePolicyFactory-metadata.html": [ - "70a5b4466617c5d5e42c69b500fbc8653403c782", + "TrustedTypePolicyFactory-metadata.tentative.html": [ + "e7772bf0d150ad687c9e51339cae0a3c4a7a1fea", [ null, { @@ -754472,9 +756255,40 @@ ] ], "video-encoder-flush.https.any.js": [ - "8f1724bc8571df44127cbb32a80915ded9b74687", + "c1ebafc4a3c9c356e6f2c6aa361bb881a0f944dc", + [ + "webcodecs/video-encoder-flush.https.any.html?h264_avc", + { + "script_metadata": [ + [ + "global", + "window,dedicatedworker" + ], + [ + "script", + "/common/media.js" + ], + [ + "script", + "/webcodecs/utils.js" + ], + [ + "script", + "/webcodecs/video-encoder-utils.js" + ], + [ + "variant", + "?vp8" + ], + [ + "variant", + "?h264_avc" + ] + ] + } + ], [ - "webcodecs/video-encoder-flush.https.any.html", + "webcodecs/video-encoder-flush.https.any.html?vp8", { "script_metadata": [ [ @@ -754492,12 +756306,20 @@ [ "script", "/webcodecs/video-encoder-utils.js" + ], + [ + "variant", + "?vp8" + ], + [ + "variant", + "?h264_avc" ] ] } ], [ - "webcodecs/video-encoder-flush.https.any.worker.html", + "webcodecs/video-encoder-flush.https.any.worker.html?h264_avc", { "script_metadata": [ [ @@ -754515,6 +756337,45 @@ [ "script", "/webcodecs/video-encoder-utils.js" + ], + [ + "variant", + "?vp8" + ], + [ + "variant", + "?h264_avc" + ] + ] + } + ], + [ + "webcodecs/video-encoder-flush.https.any.worker.html?vp8", + { + "script_metadata": [ + [ + "global", + "window,dedicatedworker" + ], + [ + "script", + "/common/media.js" + ], + [ + "script", + "/webcodecs/utils.js" + ], + [ + "script", + "/webcodecs/video-encoder-utils.js" + ], + [ + "variant", + "?vp8" + ], + [ + "variant", + "?h264_avc" ] ] } @@ -754849,7 +756710,7 @@ ] ], "videoDecoder-codec-specific.https.any.js": [ - "a3acb82ab2624970506d15680dd9730f5ff80fcb", + "1c3b8f120dbf8843d6cb61d9b82812a2ced04631", [ "webcodecs/videoDecoder-codec-specific.https.any.html?av1", { @@ -755532,6 +757393,47 @@ } ] ], + "videoFrame-copyTo-rgb.any.js": [ + "442efc4b0f487816631430b50cdd893c72767be3", + [ + "webcodecs/videoFrame-copyTo-rgb.any.html", + { + "script_metadata": [ + [ + "global", + "window,dedicatedworker" + ], + [ + "script", + "/webcodecs/videoFrame-utils.js" + ], + [ + "script", + "/webcodecs/video-encoder-utils.js" + ] + ] + } + ], + [ + "webcodecs/videoFrame-copyTo-rgb.any.worker.html", + { + "script_metadata": [ + [ + "global", + "window,dedicatedworker" + ], + [ + "script", + "/webcodecs/videoFrame-utils.js" + ], + [ + "script", + "/webcodecs/video-encoder-utils.js" + ] + ] + } + ] + ], "videoFrame-copyTo.any.js": [ "0fa57f43105fd40a01ed83664f0ef46171af876f", [ @@ -756266,6 +758168,13 @@ {} ] ], + "legacy-factory-function-builtin-properties.window.js": [ + "fc5c48aca380c0d6f44bc1331560c015e4cd3343", + [ + "webidl/ecmascript-binding/legacy-factory-function-builtin-properties.window.html", + {} + ] + ], "legacy-platform-object": { "DefineOwnProperty.html": [ "bd7ba19c1a90b66446b5e116187781dbeb786602", @@ -759960,6 +761869,57 @@ } ] ], + "parallel-compute.https.any.js": [ + "13f2757b54e4c6a691bd84133aee264eb38162db", + [ + "webnn/conformance_tests/gpu/parallel-compute.https.any.html", + { + "script_metadata": [ + [ + "title", + "test parallel WebNN API compute operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "script", + "../../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/gpu/parallel-compute.https.any.worker.html", + { + "script_metadata": [ + [ + "title", + "test parallel WebNN API compute operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "script", + "../../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ] + ], "pooling.https.any.js": [ "837bca2c71d80d77b1071595899dbc1d82cc138a", [ @@ -761185,6 +763145,57 @@ } ] ], + "parallel-compute.https.any.js": [ + "304ea5f202fa895f41853e3b0864b297c4be5a56", + [ + "webnn/conformance_tests/parallel-compute.https.any.html", + { + "script_metadata": [ + [ + "title", + "test parallel WebNN API compute operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ], + [ + "webnn/conformance_tests/parallel-compute.https.any.worker.html", + { + "script_metadata": [ + [ + "title", + "test parallel WebNN API compute operations" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "script", + "../resources/utils.js" + ], + [ + "timeout", + "long" + ] + ], + "timeout": "long" + } + ] + ], "pooling.https.any.js": [ "400d5ed37de1563261ffe71863e45752ea56f4b7", [ @@ -762186,7 +764197,7 @@ ] ], "clamp.https.any.js": [ - "85cd19a566add513e528adc8e5c2d51014ffa71b", + "126fa90e167fb344d2b41f71d85f1c3f4ed8addd", [ "webnn/validation_tests/clamp.https.any.html", { @@ -762350,7 +764361,7 @@ ] ], "conv2d.https.any.js": [ - "ffc9c2c65dff541da38886067febd4c4850ec9d8", + "7dac654951b77817b88d3ca1840dc07a6d4f4e6c", [ "webnn/validation_tests/conv2d.https.any.html", { @@ -762391,7 +764402,7 @@ ] ], "convTranspose2d.https.any.js": [ - "3c3c49566439da0e672334d9a42a71011dd35735", + "02822c52749e8c642eca2209f2a37be73393a2a1", [ "webnn/validation_tests/convTranspose2d.https.any.html", { @@ -762514,7 +764525,7 @@ ] ], "elu.https.any.js": [ - "6e842cb691a3f62b0d0b5b0a85d351fdd495ead6", + "53ec5e54ae2dc2d790f493b0669c8ca35dc34737", [ "webnn/validation_tests/elu.https.any.html", { @@ -762555,7 +764566,7 @@ ] ], "expand.https.any.js": [ - "d90ab8946891e564b5c20a13fe526531d320d413", + "088d826df7ab5415a0d03f24c902ea61aa051b06", [ "webnn/validation_tests/expand.https.any.html", { @@ -762636,8 +764647,49 @@ } ] ], + "gelu.https.any.js": [ + "c758c61f4c38477cde055438758f2fb1294708cc", + [ + "webnn/validation_tests/gelu.https.any.html", + { + "script_metadata": [ + [ + "title", + "validation tests for WebNN API gelu operation" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "script", + "../resources/utils_validation.js" + ] + ] + } + ], + [ + "webnn/validation_tests/gelu.https.any.worker.html", + { + "script_metadata": [ + [ + "title", + "validation tests for WebNN API gelu operation" + ], + [ + "global", + "window,dedicatedworker" + ], + [ + "script", + "../resources/utils_validation.js" + ] + ] + } + ] + ], "gemm.https.any.js": [ - "77ce6383ccf6a7eabee082d201e525f20ca5ce93", + "abe0ba61936b0f11a6eade254a3e58a2ec29d11b", [ "webnn/validation_tests/gemm.https.any.html", { @@ -762760,7 +764812,7 @@ ] ], "hardSigmoid.https.any.js": [ - "01b24dbc7c60b51ae6f614c2b2a1246af3051074", + "2c55d0eb9dfe21b69e88049d2570751eb2b23fb7", [ "webnn/validation_tests/hardSigmoid.https.any.html", { @@ -762924,7 +764976,7 @@ ] ], "layerNormalization.https.any.js": [ - "e9e9141aa6c2fcf482f28ebb4ec4c5dda1f9ee30", + "63f9c0dbc5855a10ae574bdcb4fddda346124497", [ "webnn/validation_tests/layerNormalization.https.any.html", { @@ -762965,7 +765017,7 @@ ] ], "leakyRelu.https.any.js": [ - "6fc19b1f0d727eea0b1da11986e840527e8bed93", + "f250b0eda66c883af5f715fe7d24d086eec3631a", [ "webnn/validation_tests/leakyRelu.https.any.html", { @@ -763006,7 +765058,7 @@ ] ], "linear.https.any.js": [ - "99c1daad3f1369dceffbd0879c43c6933033596f", + "6ec0389fc362c2aea490f5e990968144ead2d5df", [ "webnn/validation_tests/linear.https.any.html", { @@ -763129,7 +765181,7 @@ ] ], "matmul.https.any.js": [ - "6ce0d87ca1c26f67a5e20711bb7f397cc3bb202e", + "8db16242c9efb5612123333eb80d93167d754503", [ "webnn/validation_tests/matmul.https.any.html", { @@ -763170,7 +765222,7 @@ ] ], "pad.https.any.js": [ - "11c6a8f7ef21a7db38b3c5e3fb93f916a9719711", + "cc39bee4c0d1e65a09f8c96db05db74e6201874e", [ "webnn/validation_tests/pad.https.any.html", { @@ -763475,7 +765527,7 @@ ] ], "reshape.https.any.js": [ - "435551b716ab378088b657a4006296eb00c55775", + "67491fbc1684e16bcf03cf12aaac8101f70a0175", [ "webnn/validation_tests/reshape.https.any.html", { @@ -763557,7 +765609,7 @@ ] ], "slice.https.any.js": [ - "a45ecd3fcb538f94c6e845f5e21a54f7e62e9b33", + "de426216101fb29f2afa5535d7732284922e67d4", [ "webnn/validation_tests/slice.https.any.html", { @@ -763639,7 +765691,7 @@ ] ], "softplus.https.any.js": [ - "347dfcd9385b2376c851a4f60271b3848b274e09", + "3cf91d26ecb9316d8847feb84aebf9da6742e138", [ "webnn/validation_tests/softplus.https.any.html", { @@ -763721,7 +765773,7 @@ ] ], "split.https.any.js": [ - "38f31266037fb93cf1823dcaad05771b27971f31", + "6f7809744a7b6ecdceda9a9b97e50176c5b1e884", [ "webnn/validation_tests/split.https.any.html", { @@ -765701,7 +767753,7 @@ ] ], "RTCEncodedAudioFrame-metadata.https.html": [ - "1e420e6f725230a540a669f1f17fd1ebeceb78f1", + "609467b5e3ecdc2b777321da15fb64f713985082", [ null, { @@ -765746,7 +767798,7 @@ ] ], "RTCEncodedVideoFrame-metadata.https.html": [ - "a2c684c1f1594522c893f461e4bf9072caa9f0a2", + "77e1ed118f21c5a47aaff1c1548e73e06dcf9666", [ null, { @@ -810801,14 +812853,14 @@ }, "capture_screenshot": { "capture_screenshot.py": [ - "40497ce6ac7908239ea641b8c75a0a85c34613d7", + "414f5ae2d01ea63afc7b30f7f84515e6d3e554ff", [ null, {} ] ], "clip.py": [ - "8300e962b92c50c019c0afd3347c5c79cdb0c0d6", + "67d4b0d06c431c0afddc989f0b408c5b10813d42", [ null, {} @@ -811215,7 +813267,7 @@ }, "set_viewport": { "device_pixel_ratio.py": [ - "e4db779bd5dd5492af4997531fdc9c397718795b", + "76a4ef7ecd28c6977762404820f61c1b2dfd5dea", [ null, {} @@ -811743,7 +813795,7 @@ }, "response_completed": { "response_completed.py": [ - "56b94616425a2de56849b184c31bf22449985962", + "f17a522766e1f5d32555787d22818f830bf9d6ea", [ null, {} @@ -811755,18 +813807,11 @@ null, {} ] - ], - "response_completed_status.py": [ - "36e3da667e92c3b5bd4d7f7c32c66b3dc658d77e", - [ - null, - {} - ] ] }, "response_started": { "response_started.py": [ - "6c10714ca81315df7b0ab3ee634917a48977d707", + "9aa77739abcd9c74a5c5639af4d9dc7e60655888", [ null, {} diff --git a/tests/wpt/meta/css/css-animations/parsing/animation-delay-end-computed.tentative.html.ini b/tests/wpt/meta/css/css-animations/parsing/animation-delay-end-computed.tentative.html.ini deleted file mode 100644 index f4ec3b330f0..00000000000 --- a/tests/wpt/meta/css/css-animations/parsing/animation-delay-end-computed.tentative.html.ini +++ /dev/null @@ -1,12 +0,0 @@ -[animation-delay-end-computed.tentative.html] - [Property animation-delay-end value 'initial'] - expected: FAIL - - [Property animation-delay-end value '-500ms'] - expected: FAIL - - [Property animation-delay-end value 'calc(2 * 3s)'] - expected: FAIL - - [Property animation-delay-end value '20s, 10s'] - expected: FAIL diff --git a/tests/wpt/meta/css/css-animations/parsing/animation-delay-end-valid.tentative.html.ini b/tests/wpt/meta/css/css-animations/parsing/animation-delay-end-valid.tentative.html.ini deleted file mode 100644 index 0809ede9423..00000000000 --- a/tests/wpt/meta/css/css-animations/parsing/animation-delay-end-valid.tentative.html.ini +++ /dev/null @@ -1,12 +0,0 @@ -[animation-delay-end-valid.tentative.html] - [e.style['animation-delay-end'\] = "-5ms" should set the property value] - expected: FAIL - - [e.style['animation-delay-end'\] = "0s" should set the property value] - expected: FAIL - - [e.style['animation-delay-end'\] = "10s" should set the property value] - expected: FAIL - - [e.style['animation-delay-end'\] = "20s, 10s" should set the property value] - expected: FAIL diff --git a/tests/wpt/meta/css/css-animations/parsing/animation-delay-shorthand-computed.html.ini b/tests/wpt/meta/css/css-animations/parsing/animation-delay-shorthand-computed.html.ini deleted file mode 100644 index 1967721ce03..00000000000 --- a/tests/wpt/meta/css/css-animations/parsing/animation-delay-shorthand-computed.html.ini +++ /dev/null @@ -1,60 +0,0 @@ -[animation-delay-shorthand-computed.html] - [Property animation-delay value '1s 2s'] - expected: FAIL - - [Property animation-delay value '1s 2s, 3s'] - expected: FAIL - - [Property animation-delay value '1s, 2s 3s'] - expected: FAIL - - [Property animation-delay value 'cover'] - expected: FAIL - - [Property animation-delay value 'contain'] - expected: FAIL - - [Property animation-delay value 'enter'] - expected: FAIL - - [Property animation-delay value 'exit'] - expected: FAIL - - [Property animation-delay value 'enter, exit'] - expected: FAIL - - [Property animation-delay value 'enter 0% enter 100%'] - expected: FAIL - - [Property animation-delay value 'exit 0% exit 100%'] - expected: FAIL - - [Property animation-delay value 'cover 0% cover 100%'] - expected: FAIL - - [Property animation-delay value 'contain 0% contain 100%'] - expected: FAIL - - [Property animation-delay value 'cover 50%'] - expected: FAIL - - [Property animation-delay value 'contain 50%'] - expected: FAIL - - [Property animation-delay value 'enter 50%'] - expected: FAIL - - [Property animation-delay value 'exit 50%'] - expected: FAIL - - [Property animation-delay value 'enter 50% 0s'] - expected: FAIL - - [Property animation-delay value '0s enter 50%'] - expected: FAIL - - [Property animation-delay value 'enter 50% exit 50%'] - expected: FAIL - - [Property animation-delay value 'cover 50% enter 50%, contain 50% exit 50%'] - expected: FAIL diff --git a/tests/wpt/meta/css/css-animations/parsing/animation-delay-shorthand.html.ini b/tests/wpt/meta/css/css-animations/parsing/animation-delay-shorthand.html.ini deleted file mode 100644 index a67cdfc1609..00000000000 --- a/tests/wpt/meta/css/css-animations/parsing/animation-delay-shorthand.html.ini +++ /dev/null @@ -1,150 +0,0 @@ -[animation-delay-shorthand.html] - [e.style['animation-delay'\] = "1s 2s" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "1s 2s, 3s" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "1s, 2s 3s" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "cover" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "contain" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "enter" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "exit" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "enter, exit" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "enter 0% enter 100%" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "exit 0% exit 100%" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "cover 0% cover 100%" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "contain 0% contain 100%" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "cover 50%" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "contain 50%" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "enter 50%" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "exit 50%" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "enter 50% 0s" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "0s enter 50%" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "enter 50% exit 50%" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "cover 50% enter 50%, contain 50% exit 50%" should set the property value] - expected: FAIL - - [e.style['animation-delay'\] = "1s 2s" should set animation-delay-end] - expected: FAIL - - [e.style['animation-delay'\] = "1s 2s" should set animation-delay-start] - expected: FAIL - - [e.style['animation-delay'\] = "1s 2s" should not set unrelated longhands] - expected: FAIL - - [e.style['animation-delay'\] = "1s" should set animation-delay-end] - expected: FAIL - - [e.style['animation-delay'\] = "1s" should set animation-delay-start] - expected: FAIL - - [e.style['animation-delay'\] = "1s" should not set unrelated longhands] - expected: FAIL - - [e.style['animation-delay'\] = "cover" should set animation-delay-end] - expected: FAIL - - [e.style['animation-delay'\] = "cover" should set animation-delay-start] - expected: FAIL - - [e.style['animation-delay'\] = "cover" should not set unrelated longhands] - expected: FAIL - - [e.style['animation-delay'\] = "contain" should set animation-delay-end] - expected: FAIL - - [e.style['animation-delay'\] = "contain" should set animation-delay-start] - expected: FAIL - - [e.style['animation-delay'\] = "contain" should not set unrelated longhands] - expected: FAIL - - [e.style['animation-delay'\] = "enter 10% exit 20%" should set animation-delay-end] - expected: FAIL - - [e.style['animation-delay'\] = "enter 10% exit 20%" should set animation-delay-start] - expected: FAIL - - [e.style['animation-delay'\] = "enter 10% exit 20%" should not set unrelated longhands] - expected: FAIL - - [e.style['animation-delay'\] = "1s 2s, 3s 4s" should set animation-delay-end] - expected: FAIL - - [e.style['animation-delay'\] = "1s 2s, 3s 4s" should set animation-delay-start] - expected: FAIL - - [e.style['animation-delay'\] = "1s 2s, 3s 4s" should not set unrelated longhands] - expected: FAIL - - [e.style['animation-delay'\] = "1s 2s, 3s, 4s 5s" should set animation-delay-end] - expected: FAIL - - [e.style['animation-delay'\] = "1s 2s, 3s, 4s 5s" should set animation-delay-start] - expected: FAIL - - [e.style['animation-delay'\] = "1s 2s, 3s, 4s 5s" should not set unrelated longhands] - expected: FAIL - - [e.style['animation-delay'\] = "enter, exit" should set animation-delay-end] - expected: FAIL - - [e.style['animation-delay'\] = "enter, exit" should set animation-delay-start] - expected: FAIL - - [e.style['animation-delay'\] = "enter, exit" should not set unrelated longhands] - expected: FAIL - - [e.style['animation-delay'\] = "enter 0%, exit" should set animation-delay-end] - expected: FAIL - - [e.style['animation-delay'\] = "enter 0%, exit" should set animation-delay-start] - expected: FAIL - - [e.style['animation-delay'\] = "enter 0%, exit" should not set unrelated longhands] - expected: FAIL - - [e.style['animation-delay'\] = "enter 0% 1s, 2s exit 50%" should set animation-delay-end] - expected: FAIL - - [e.style['animation-delay'\] = "enter 0% 1s, 2s exit 50%" should set animation-delay-start] - expected: FAIL - - [e.style['animation-delay'\] = "enter 0% 1s, 2s exit 50%" should not set unrelated longhands] - expected: FAIL diff --git a/tests/wpt/meta/css/css-animations/parsing/animation-delay-start-computed.tentative.html.ini b/tests/wpt/meta/css/css-animations/parsing/animation-delay-start-computed.tentative.html.ini deleted file mode 100644 index 8341379e4d8..00000000000 --- a/tests/wpt/meta/css/css-animations/parsing/animation-delay-start-computed.tentative.html.ini +++ /dev/null @@ -1,12 +0,0 @@ -[animation-delay-start-computed.tentative.html] - [Property animation-delay-start value 'initial'] - expected: FAIL - - [Property animation-delay-start value '-500ms'] - expected: FAIL - - [Property animation-delay-start value 'calc(2 * 3s)'] - expected: FAIL - - [Property animation-delay-start value '20s, 10s'] - expected: FAIL diff --git a/tests/wpt/meta/css/css-animations/parsing/animation-delay-start-valid.tentative.html.ini b/tests/wpt/meta/css/css-animations/parsing/animation-delay-start-valid.tentative.html.ini deleted file mode 100644 index 4ad3bb1e96b..00000000000 --- a/tests/wpt/meta/css/css-animations/parsing/animation-delay-start-valid.tentative.html.ini +++ /dev/null @@ -1,12 +0,0 @@ -[animation-delay-start-valid.tentative.html] - [e.style['animation-delay-start'\] = "-5ms" should set the property value] - expected: FAIL - - [e.style['animation-delay-start'\] = "0s" should set the property value] - expected: FAIL - - [e.style['animation-delay-start'\] = "10s" should set the property value] - expected: FAIL - - [e.style['animation-delay-start'\] = "20s, 10s" should set the property value] - expected: FAIL diff --git a/tests/wpt/meta/css/css-cascade/scope-pseudo-element.html.ini b/tests/wpt/meta/css/css-cascade/scope-pseudo-element.html.ini new file mode 100644 index 00000000000..80b96e894af --- /dev/null +++ b/tests/wpt/meta/css/css-cascade/scope-pseudo-element.html.ini @@ -0,0 +1,2 @@ +[scope-pseudo-element.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-color/hsl-clamp-negative-saturation.html.ini b/tests/wpt/meta/css/css-color/hsl-clamp-negative-saturation.html.ini new file mode 100644 index 00000000000..345d39e2622 --- /dev/null +++ b/tests/wpt/meta/css/css-color/hsl-clamp-negative-saturation.html.ini @@ -0,0 +1,2 @@ +[hsl-clamp-negative-saturation.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-color/hsla-clamp-negative-saturation.html.ini b/tests/wpt/meta/css/css-color/hsla-clamp-negative-saturation.html.ini new file mode 100644 index 00000000000..679c4314098 --- /dev/null +++ b/tests/wpt/meta/css/css-color/hsla-clamp-negative-saturation.html.ini @@ -0,0 +1,2 @@ +[hsla-clamp-negative-saturation.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-color/t424-hsl-clip-outside-gamut-b.xht.ini b/tests/wpt/meta/css/css-color/t424-hsl-clip-outside-gamut-b.xht.ini deleted file mode 100644 index f843b938045..00000000000 --- a/tests/wpt/meta/css/css-color/t424-hsl-clip-outside-gamut-b.xht.ini +++ /dev/null @@ -1,2 +0,0 @@ -[t424-hsl-clip-outside-gamut-b.xht] - expected: FAIL diff --git a/tests/wpt/meta/css/css-color/t425-hsla-clip-outside-device-gamut-b.xht.ini b/tests/wpt/meta/css/css-color/t425-hsla-clip-outside-device-gamut-b.xht.ini deleted file mode 100644 index 8dc18095cf7..00000000000 --- a/tests/wpt/meta/css/css-color/t425-hsla-clip-outside-device-gamut-b.xht.ini +++ /dev/null @@ -1,2 +0,0 @@ -[t425-hsla-clip-outside-device-gamut-b.xht] - expected: FAIL diff --git a/tests/wpt/meta/css/css-flexbox/intrinsic-size/col-wrap-020.html.ini b/tests/wpt/meta/css/css-flexbox/intrinsic-size/col-wrap-020.html.ini new file mode 100644 index 00000000000..5895cfc6764 --- /dev/null +++ b/tests/wpt/meta/css/css-flexbox/intrinsic-size/col-wrap-020.html.ini @@ -0,0 +1,2 @@ +[col-wrap-020.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-fonts/matching/font-unicode-PUA-primary-font.html.ini b/tests/wpt/meta/css/css-fonts/matching/font-unicode-PUA-primary-font.html.ini new file mode 100644 index 00000000000..4e0025de6c7 --- /dev/null +++ b/tests/wpt/meta/css/css-fonts/matching/font-unicode-PUA-primary-font.html.ini @@ -0,0 +1,2 @@ +[font-unicode-PUA-primary-font.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-fonts/variation-sequences.html.ini b/tests/wpt/meta/css/css-fonts/variation-sequences.html.ini new file mode 100644 index 00000000000..c9053836d50 --- /dev/null +++ b/tests/wpt/meta/css/css-fonts/variation-sequences.html.ini @@ -0,0 +1,2 @@ +[variation-sequences.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-fonts/variations/font-weight-matching.html.ini b/tests/wpt/meta/css/css-fonts/variations/font-weight-matching.html.ini index 94ac95b4e1b..18b51d5e7a9 100644 --- a/tests/wpt/meta/css/css-fonts/variations/font-weight-matching.html.ini +++ b/tests/wpt/meta/css/css-fonts/variations/font-weight-matching.html.ini @@ -1,36 +1,18 @@ [font-weight-matching.html] - [Test @font-face matching for weight 99] - expected: FAIL - [Test @font-face matching for weight 600] expected: FAIL - [Test @font-face matching for weight 1000] - expected: FAIL - [Test @font-face matching for weight 470] expected: FAIL - [Test @font-face matching for weight 900] - expected: FAIL - - [Test @font-face matching for weight 400] - expected: FAIL - - [Test @font-face matching for weight 750] - expected: FAIL - [Test @font-face matching for weight 751] expected: FAIL [Test @font-face matching for weight 399] expected: FAIL - [Test @font-face matching for weight 500] - expected: FAIL - - [Test @font-face matching for weight 250] + [Test @font-face matching for weight 420] expected: FAIL - [Test @font-face matching for weight 420] + [Test @font-face matching for weight 249] expected: FAIL diff --git a/tests/wpt/meta/css/css-pseudo/highlight-cascade-002.html.ini b/tests/wpt/meta/css/css-pseudo/highlight-cascade-002.html.ini deleted file mode 100644 index 59c9d5fe2c7..00000000000 --- a/tests/wpt/meta/css/css-pseudo/highlight-cascade-002.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[highlight-cascade-002.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-pseudo/highlight-cascade-009.html.ini b/tests/wpt/meta/css/css-pseudo/highlight-cascade-009.html.ini deleted file mode 100644 index ca26253afa4..00000000000 --- a/tests/wpt/meta/css/css-pseudo/highlight-cascade-009.html.ini +++ /dev/null @@ -1,9 +0,0 @@ -[highlight-cascade-009.html] - [body ::selection uses its own custom property] - expected: FAIL - - [div::selection inherits a custom property] - expected: FAIL - - [div::selection uses its own custom property] - expected: FAIL diff --git a/tests/wpt/meta/css/css-pseudo/cascade-highlight-001.html.ini b/tests/wpt/meta/css/css-pseudo/highlight-cascade/cascade-highlight-001.html.ini index c89aebb5ebb..c89aebb5ebb 100644 --- a/tests/wpt/meta/css/css-pseudo/cascade-highlight-001.html.ini +++ b/tests/wpt/meta/css/css-pseudo/highlight-cascade/cascade-highlight-001.html.ini diff --git a/tests/wpt/meta/css/css-pseudo/cascade-highlight-002.html.ini b/tests/wpt/meta/css/css-pseudo/highlight-cascade/cascade-highlight-002.html.ini index dd2d810e286..dd2d810e286 100644 --- a/tests/wpt/meta/css/css-pseudo/cascade-highlight-002.html.ini +++ b/tests/wpt/meta/css/css-pseudo/highlight-cascade/cascade-highlight-002.html.ini diff --git a/tests/wpt/meta/css/css-pseudo/cascade-highlight-004.html.ini b/tests/wpt/meta/css/css-pseudo/highlight-cascade/cascade-highlight-004.html.ini index 5630d0776bd..5630d0776bd 100644 --- a/tests/wpt/meta/css/css-pseudo/cascade-highlight-004.html.ini +++ b/tests/wpt/meta/css/css-pseudo/highlight-cascade/cascade-highlight-004.html.ini diff --git a/tests/wpt/meta/css/css-pseudo/cascade-highlight-005.html.ini b/tests/wpt/meta/css/css-pseudo/highlight-cascade/cascade-highlight-005.html.ini index e0dd6427940..e0dd6427940 100644 --- a/tests/wpt/meta/css/css-pseudo/cascade-highlight-005.html.ini +++ b/tests/wpt/meta/css/css-pseudo/highlight-cascade/cascade-highlight-005.html.ini diff --git a/tests/wpt/meta/css/css-pseudo/highlight-cascade-007.html.ini b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-cascade-007.html.ini index 99307641b90..99307641b90 100644 --- a/tests/wpt/meta/css/css-pseudo/highlight-cascade-007.html.ini +++ b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-cascade-007.html.ini diff --git a/tests/wpt/meta/css/css-pseudo/highlight-currentcolor-computed-inheritance.html.ini b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-currentcolor-computed-inheritance.html.ini index 99456944d45..99456944d45 100644 --- a/tests/wpt/meta/css/css-pseudo/highlight-currentcolor-computed-inheritance.html.ini +++ b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-currentcolor-computed-inheritance.html.ini diff --git a/tests/wpt/meta/css/css-pseudo/highlight-currentcolor-computed.html.ini b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-currentcolor-computed.html.ini index d29df11928e..d29df11928e 100644 --- a/tests/wpt/meta/css/css-pseudo/highlight-currentcolor-computed.html.ini +++ b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-currentcolor-computed.html.ini diff --git a/tests/wpt/meta/css/css-pseudo/highlight-currentcolor-painting-properties-001.html.ini b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-001.html.ini index 3919445a963..3919445a963 100644 --- a/tests/wpt/meta/css/css-pseudo/highlight-currentcolor-painting-properties-001.html.ini +++ b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-001.html.ini diff --git a/tests/wpt/meta/css/css-pseudo/highlight-currentcolor-painting-properties-002.html.ini b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-002.html.ini index 44b19e7edb4..44b19e7edb4 100644 --- a/tests/wpt/meta/css/css-pseudo/highlight-currentcolor-painting-properties-002.html.ini +++ b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-002.html.ini diff --git a/tests/wpt/meta/css/css-pseudo/highlight-currentcolor-painting-text-shadow-001.html.ini b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-001.html.ini index 9cbcacbddd7..9cbcacbddd7 100644 --- a/tests/wpt/meta/css/css-pseudo/highlight-currentcolor-painting-text-shadow-001.html.ini +++ b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-001.html.ini diff --git a/tests/wpt/meta/css/css-pseudo/highlight-currentcolor-painting-text-shadow-002.html.ini b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-002.html.ini index e5976d08881..e5976d08881 100644 --- a/tests/wpt/meta/css/css-pseudo/highlight-currentcolor-painting-text-shadow-002.html.ini +++ b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-002.html.ini diff --git a/tests/wpt/meta/css/css-pseudo/highlight-currentcolor-root-explicit-default-002.html.ini b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-002.html.ini index ad2279b7044..ad2279b7044 100644 --- a/tests/wpt/meta/css/css-pseudo/highlight-currentcolor-root-explicit-default-002.html.ini +++ b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-002.html.ini diff --git a/tests/wpt/meta/css/css-pseudo/highlight-currentcolor-root-implicit-default-001.html.ini b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-implicit-default-001.html.ini index 1310b157b1a..1310b157b1a 100644 --- a/tests/wpt/meta/css/css-pseudo/highlight-currentcolor-root-implicit-default-001.html.ini +++ b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-implicit-default-001.html.ini diff --git a/tests/wpt/meta/css/css-pseudo/highlight-currentcolor-root-implicit-default-002.html.ini b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-implicit-default-002.html.ini index 5d7f1ec0af7..5d7f1ec0af7 100644 --- a/tests/wpt/meta/css/css-pseudo/highlight-currentcolor-root-implicit-default-002.html.ini +++ b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-implicit-default-002.html.ini diff --git a/tests/wpt/meta/css/css-pseudo/highlight-paired-cascade-004.html.ini b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-paired-cascade-004.html.ini index d0f4abb4abb..d0f4abb4abb 100644 --- a/tests/wpt/meta/css/css-pseudo/highlight-paired-cascade-004.html.ini +++ b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-paired-cascade-004.html.ini diff --git a/tests/wpt/meta/css/css-pseudo/highlight-pseudos-computed.html.ini b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-pseudos-computed.html.ini index 7c4acf428f7..7c4acf428f7 100644 --- a/tests/wpt/meta/css/css-pseudo/highlight-pseudos-computed.html.ini +++ b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-pseudos-computed.html.ini diff --git a/tests/wpt/meta/css/css-pseudo/highlight-pseudos-inheritance-computed-001.html.ini b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-pseudos-inheritance-computed-001.html.ini index a4e4bdc95a6..a4e4bdc95a6 100644 --- a/tests/wpt/meta/css/css-pseudo/highlight-pseudos-inheritance-computed-001.html.ini +++ b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-pseudos-inheritance-computed-001.html.ini diff --git a/tests/wpt/meta/css/css-pseudo/highlight-pseudos-visited-computed-001.html.ini b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-pseudos-visited-computed-001.html.ini index 267ebaa918b..267ebaa918b 100644 --- a/tests/wpt/meta/css/css-pseudo/highlight-pseudos-visited-computed-001.html.ini +++ b/tests/wpt/meta/css/css-pseudo/highlight-cascade/highlight-pseudos-visited-computed-001.html.ini diff --git a/tests/wpt/meta/css/css-pseudo/highlight-painting-shadows-horizontal.html.ini b/tests/wpt/meta/css/css-pseudo/highlight-painting-shadows-horizontal.html.ini new file mode 100644 index 00000000000..b15ba58b7ca --- /dev/null +++ b/tests/wpt/meta/css/css-pseudo/highlight-painting-shadows-horizontal.html.ini @@ -0,0 +1,2 @@ +[highlight-painting-shadows-horizontal.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-pseudo/highlight-painting-shadows-vertical.html.ini b/tests/wpt/meta/css/css-pseudo/highlight-painting-shadows-vertical.html.ini new file mode 100644 index 00000000000..b39e4c85fcd --- /dev/null +++ b/tests/wpt/meta/css/css-pseudo/highlight-painting-shadows-vertical.html.ini @@ -0,0 +1,2 @@ +[highlight-painting-shadows-vertical.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-pseudo/target-text-dynamic-004.html.ini b/tests/wpt/meta/css/css-pseudo/target-text-dynamic-004.html.ini deleted file mode 100644 index c8b5a4ab11b..00000000000 --- a/tests/wpt/meta/css/css-pseudo/target-text-dynamic-004.html.ini +++ /dev/null @@ -1,2 +0,0 @@ -[target-text-dynamic-004.html] - expected: FAIL diff --git a/tests/wpt/meta/css/css-pseudo/target-text-shadow-horizontal.html.ini b/tests/wpt/meta/css/css-pseudo/target-text-shadow-horizontal.html.ini new file mode 100644 index 00000000000..d0ac640e63a --- /dev/null +++ b/tests/wpt/meta/css/css-pseudo/target-text-shadow-horizontal.html.ini @@ -0,0 +1,2 @@ +[target-text-shadow-horizontal.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-pseudo/target-text-shadow-vertical.html.ini b/tests/wpt/meta/css/css-pseudo/target-text-shadow-vertical.html.ini new file mode 100644 index 00000000000..12979318989 --- /dev/null +++ b/tests/wpt/meta/css/css-pseudo/target-text-shadow-vertical.html.ini @@ -0,0 +1,2 @@ +[target-text-shadow-vertical.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-transitions/shadow-root-insertion.html.ini b/tests/wpt/meta/css/css-transitions/shadow-root-insertion.html.ini new file mode 100644 index 00000000000..bc9bbbcf84d --- /dev/null +++ b/tests/wpt/meta/css/css-transitions/shadow-root-insertion.html.ini @@ -0,0 +1,3 @@ +[shadow-root-insertion.html] + [addition of a shadow root should not cancel in-flight transitions] + expected: FAIL diff --git a/tests/wpt/meta/css/css-values/calc-rounding-003.html.ini b/tests/wpt/meta/css/css-values/calc-rounding-003.html.ini new file mode 100644 index 00000000000..b82a3cd9292 --- /dev/null +++ b/tests/wpt/meta/css/css-values/calc-rounding-003.html.ini @@ -0,0 +1,2 @@ +[calc-rounding-003.html] + expected: FAIL diff --git a/tests/wpt/meta/css/css-values/calc-size/animation/calc-size-height-interpolation.tentative.html.ini b/tests/wpt/meta/css/css-values/calc-size/animation/calc-size-height-interpolation.tentative.html.ini index c6d325ba2e1..a0c94fe0a1c 100644 --- a/tests/wpt/meta/css/css-values/calc-size/animation/calc-size-height-interpolation.tentative.html.ini +++ b/tests/wpt/meta/css/css-values/calc-size/animation/calc-size-height-interpolation.tentative.html.ini @@ -2764,3 +2764,123 @@ [Web Animations: property <height> from [calc-size(37px, 200px)\] to [calc-size(37px, size * 2 + 3% + 17px)\] at (1.25) should be [75px\]] expected: FAIL + + [CSS Transitions: property <height> from [calc-size(auto, size)\] to [50%\] at (-0.25) should be [87.5px\]] + expected: FAIL + + [CSS Transitions: property <height> from [calc-size(auto, size)\] to [50%\] at (0) should be [100px\]] + expected: FAIL + + [CSS Transitions: property <height> from [calc-size(auto, size)\] to [50%\] at (0.75) should be [137.5px\]] + expected: FAIL + + [CSS Transitions: property <height> from [calc-size(auto, size)\] to [50%\] at (1) should be [150px\]] + expected: FAIL + + [CSS Transitions: property <height> from [calc-size(auto, size)\] to [50%\] at (1.25) should be [162.5px\]] + expected: FAIL + + [CSS Transitions with transition: all: property <height> from [calc-size(auto, size)\] to [50%\] at (-0.25) should be [87.5px\]] + expected: FAIL + + [CSS Transitions with transition: all: property <height> from [calc-size(auto, size)\] to [50%\] at (0) should be [100px\]] + expected: FAIL + + [CSS Transitions with transition: all: property <height> from [calc-size(auto, size)\] to [50%\] at (0.75) should be [137.5px\]] + expected: FAIL + + [CSS Transitions with transition: all: property <height> from [calc-size(auto, size)\] to [50%\] at (1) should be [150px\]] + expected: FAIL + + [CSS Transitions with transition: all: property <height> from [calc-size(auto, size)\] to [50%\] at (1.25) should be [162.5px\]] + expected: FAIL + + [CSS Animations: property <height> from [calc-size(auto, size)\] to [50%\] at (-0.25) should be [87.5px\]] + expected: FAIL + + [CSS Animations: property <height> from [calc-size(auto, size)\] to [50%\] at (0) should be [100px\]] + expected: FAIL + + [CSS Animations: property <height> from [calc-size(auto, size)\] to [50%\] at (0.75) should be [137.5px\]] + expected: FAIL + + [CSS Animations: property <height> from [calc-size(auto, size)\] to [50%\] at (1) should be [150px\]] + expected: FAIL + + [CSS Animations: property <height> from [calc-size(auto, size)\] to [50%\] at (1.25) should be [162.5px\]] + expected: FAIL + + [Web Animations: property <height> from [calc-size(auto, size)\] to [50%\] at (-0.25) should be [87.5px\]] + expected: FAIL + + [Web Animations: property <height> from [calc-size(auto, size)\] to [50%\] at (0) should be [100px\]] + expected: FAIL + + [Web Animations: property <height> from [calc-size(auto, size)\] to [50%\] at (0.75) should be [137.5px\]] + expected: FAIL + + [Web Animations: property <height> from [calc-size(auto, size)\] to [50%\] at (1) should be [150px\]] + expected: FAIL + + [Web Animations: property <height> from [calc-size(auto, size)\] to [50%\] at (1.25) should be [162.5px\]] + expected: FAIL + + [CSS Transitions: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (-0.25) should be [250px\]] + expected: FAIL + + [CSS Transitions: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (0) should be [200px\]] + expected: FAIL + + [CSS Transitions: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (0.75) should be [50px\]] + expected: FAIL + + [CSS Transitions: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (1) should be [0px\]] + expected: FAIL + + [CSS Transitions: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (1.25) should be [0px\]] + expected: FAIL + + [CSS Transitions with transition: all: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (-0.25) should be [250px\]] + expected: FAIL + + [CSS Transitions with transition: all: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (0) should be [200px\]] + expected: FAIL + + [CSS Transitions with transition: all: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (0.75) should be [50px\]] + expected: FAIL + + [CSS Transitions with transition: all: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (1) should be [0px\]] + expected: FAIL + + [CSS Transitions with transition: all: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (1.25) should be [0px\]] + expected: FAIL + + [CSS Animations: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (-0.25) should be [250px\]] + expected: FAIL + + [CSS Animations: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (0) should be [200px\]] + expected: FAIL + + [CSS Animations: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (0.75) should be [50px\]] + expected: FAIL + + [CSS Animations: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (1) should be [0px\]] + expected: FAIL + + [CSS Animations: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (1.25) should be [0px\]] + expected: FAIL + + [Web Animations: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (-0.25) should be [250px\]] + expected: FAIL + + [Web Animations: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (0) should be [200px\]] + expected: FAIL + + [Web Animations: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (0.75) should be [50px\]] + expected: FAIL + + [Web Animations: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (1) should be [0px\]] + expected: FAIL + + [Web Animations: property <height> from [calc-size(auto, size * 2)\] to [50%\] at (1.25) should be [0px\]] + expected: FAIL diff --git a/tests/wpt/meta/css/css-values/calc-size/calc-size-parsing.tentative.html.ini b/tests/wpt/meta/css/css-values/calc-size/calc-size-parsing.tentative.html.ini index 15063a73f41..998e202dbc3 100644 --- a/tests/wpt/meta/css/css-values/calc-size/calc-size-parsing.tentative.html.ini +++ b/tests/wpt/meta/css/css-values/calc-size/calc-size-parsing.tentative.html.ini @@ -55,3 +55,21 @@ [e.style['width'\] = "calc-size(10px, sign(size) * size)" should set the property value] expected: FAIL + + [e.style['width'\] = "calc-size(30px)" should set the property value] + expected: FAIL + + [e.style['width'\] = "calc-size(min(30px, 2em))" should set the property value] + expected: FAIL + + [e.style['width'\] = "calc-size(calc-size(any, 30px))" should set the property value] + expected: FAIL + + [e.style['width'\] = "calc-size(fit-content)" should set the property value] + expected: FAIL + + [e.style['width'\] = "calc-size(calc-size(fit-content, size * 2))" should set the property value] + expected: FAIL + + [e.style['width'\] = "calc-size(calc-size(30px))" should set the property value] + expected: FAIL diff --git a/tests/wpt/meta/css/css-values/container-progress-computed.tentative.html.ini b/tests/wpt/meta/css/css-values/container-progress-computed.tentative.html.ini index e83399384fa..7bfa9700dc5 100644 --- a/tests/wpt/meta/css/css-values/container-progress-computed.tentative.html.ini +++ b/tests/wpt/meta/css/css-values/container-progress-computed.tentative.html.ini @@ -46,3 +46,15 @@ [calc(container-progress(width of my-container-2 from 0px to 1px) * 1deg) should be used-value-equivalent to 5051deg] expected: FAIL + + [container-progress() width fallback for non-existing container name] + expected: FAIL + + [container-progress() height fallback for non-existing container names] + expected: FAIL + + [container-progress() width fallback for out of scope container] + expected: FAIL + + [container-progress() height fallback for out of scope container] + expected: FAIL diff --git a/tests/wpt/meta/css/cssom-view/MediaQueryList-addListener-removeListener.html.ini b/tests/wpt/meta/css/cssom-view/MediaQueryList-addListener-removeListener.html.ini index d52a3e77a70..314dca9c1f5 100644 --- a/tests/wpt/meta/css/cssom-view/MediaQueryList-addListener-removeListener.html.ini +++ b/tests/wpt/meta/css/cssom-view/MediaQueryList-addListener-removeListener.html.ini @@ -1,6 +1,3 @@ [MediaQueryList-addListener-removeListener.html] [listeners are called when <iframe> is resized] expected: FAIL - - [listeners are called correct number of times] - expected: FAIL diff --git a/tests/wpt/meta/css/mediaqueries/prefers-color-scheme-svg-as-image.html.ini b/tests/wpt/meta/css/mediaqueries/prefers-color-scheme-svg-as-image.html.ini new file mode 100644 index 00000000000..2477c232c9a --- /dev/null +++ b/tests/wpt/meta/css/mediaqueries/prefers-color-scheme-svg-as-image.html.ini @@ -0,0 +1,2 @@ +[prefers-color-scheme-svg-as-image.html] + expected: FAIL diff --git a/tests/wpt/meta/dom/idlharness.window.js.ini b/tests/wpt/meta/dom/idlharness.window.js.ini index 5c3870f4178..3f173557cc7 100644 --- a/tests/wpt/meta/dom/idlharness.window.js.ini +++ b/tests/wpt/meta/dom/idlharness.window.js.ini @@ -720,3 +720,60 @@ [ShadowRoot interface: attribute clonable] expected: FAIL + + [Document interface: operation prepend((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [Document interface: operation append((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [Document interface: operation replaceChildren((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [DocumentType interface: operation before((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [DocumentType interface: operation after((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [DocumentType interface: operation replaceWith((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [DocumentFragment interface: operation prepend((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [DocumentFragment interface: operation append((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [DocumentFragment interface: operation replaceChildren((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [ShadowRoot interface: attribute serializable] + expected: FAIL + + [Element interface: operation prepend((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [Element interface: operation append((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [Element interface: operation replaceChildren((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [Element interface: operation before((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [Element interface: operation after((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [Element interface: operation replaceWith((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [CharacterData interface: operation before((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [CharacterData interface: operation after((Node or TrustedScript or DOMString)...)] + expected: FAIL + + [CharacterData interface: operation replaceWith((Node or TrustedScript or DOMString)...)] + expected: FAIL diff --git a/tests/wpt/meta/dom/nodes/moveBefore/tentative/continue-css-animation-left.html.ini b/tests/wpt/meta/dom/nodes/moveBefore/tentative/continue-css-animation-left.html.ini new file mode 100644 index 00000000000..f109b605d2c --- /dev/null +++ b/tests/wpt/meta/dom/nodes/moveBefore/tentative/continue-css-animation-left.html.ini @@ -0,0 +1,3 @@ +[continue-css-animation-left.html] + [Node.moveBefore should preserve CSS animation state (left)] + expected: FAIL diff --git a/tests/wpt/meta/dom/nodes/moveBefore/tentative/continue-css-animation-transform.html.ini b/tests/wpt/meta/dom/nodes/moveBefore/tentative/continue-css-animation-transform.html.ini new file mode 100644 index 00000000000..05654dd2a4e --- /dev/null +++ b/tests/wpt/meta/dom/nodes/moveBefore/tentative/continue-css-animation-transform.html.ini @@ -0,0 +1,3 @@ +[continue-css-animation-transform.html] + [Node.moveBefore should preserve CSS animation state (transform)] + expected: FAIL diff --git a/tests/wpt/meta/dom/nodes/moveBefore/tentative/continue-css-transition-left-pseudo.html.ini b/tests/wpt/meta/dom/nodes/moveBefore/tentative/continue-css-transition-left-pseudo.html.ini new file mode 100644 index 00000000000..a6273f52a31 --- /dev/null +++ b/tests/wpt/meta/dom/nodes/moveBefore/tentative/continue-css-transition-left-pseudo.html.ini @@ -0,0 +1,4 @@ +[continue-css-transition-left-pseudo.html] + expected: TIMEOUT + [Node.moveBefore should preserve CSS transition state on pseudo-elements (left)] + expected: TIMEOUT diff --git a/tests/wpt/meta/dom/nodes/moveBefore/tentative/continue-css-transition-left.html.ini b/tests/wpt/meta/dom/nodes/moveBefore/tentative/continue-css-transition-left.html.ini new file mode 100644 index 00000000000..60f17eb404b --- /dev/null +++ b/tests/wpt/meta/dom/nodes/moveBefore/tentative/continue-css-transition-left.html.ini @@ -0,0 +1,3 @@ +[continue-css-transition-left.html] + [Node.moveBefore should preserve CSS transition state (left)] + expected: FAIL diff --git a/tests/wpt/meta/dom/nodes/moveBefore/tentative/continue-css-transition-transform-pseudo.html.ini b/tests/wpt/meta/dom/nodes/moveBefore/tentative/continue-css-transition-transform-pseudo.html.ini new file mode 100644 index 00000000000..67a8527bd20 --- /dev/null +++ b/tests/wpt/meta/dom/nodes/moveBefore/tentative/continue-css-transition-transform-pseudo.html.ini @@ -0,0 +1,4 @@ +[continue-css-transition-transform-pseudo.html] + expected: TIMEOUT + [Node.moveBefore should preserve CSS transition state on pseudo-elements (transform)] + expected: TIMEOUT diff --git a/tests/wpt/meta/dom/nodes/moveBefore/tentative/continue-css-transition-transform.html.ini b/tests/wpt/meta/dom/nodes/moveBefore/tentative/continue-css-transition-transform.html.ini new file mode 100644 index 00000000000..bc2827631f7 --- /dev/null +++ b/tests/wpt/meta/dom/nodes/moveBefore/tentative/continue-css-transition-transform.html.ini @@ -0,0 +1,4 @@ +[continue-css-transition-transform.html] + expected: TIMEOUT + [Node.moveBefore should preserve CSS transition state (transform)] + expected: TIMEOUT diff --git a/tests/wpt/meta/dom/nodes/moveBefore/tentative/css-animation-commit-styles.html.ini b/tests/wpt/meta/dom/nodes/moveBefore/tentative/css-animation-commit-styles.html.ini new file mode 100644 index 00000000000..e38f3a2648b --- /dev/null +++ b/tests/wpt/meta/dom/nodes/moveBefore/tentative/css-animation-commit-styles.html.ini @@ -0,0 +1,3 @@ +[css-animation-commit-styles.html] + [Calling commitStyles after Node.moveBefore should commit mid-transition value] + expected: FAIL diff --git a/tests/wpt/meta/dom/nodes/moveBefore/tentative/css-transition-cross-document.html.ini b/tests/wpt/meta/dom/nodes/moveBefore/tentative/css-transition-cross-document.html.ini new file mode 100644 index 00000000000..eccb2a04a6a --- /dev/null +++ b/tests/wpt/meta/dom/nodes/moveBefore/tentative/css-transition-cross-document.html.ini @@ -0,0 +1,3 @@ +[css-transition-cross-document.html] + [Moving a transition across documents should reset its state] + expected: FAIL diff --git a/tests/wpt/meta/dom/nodes/moveBefore/tentative/css-transition-cross-shadow.html.ini b/tests/wpt/meta/dom/nodes/moveBefore/tentative/css-transition-cross-shadow.html.ini new file mode 100644 index 00000000000..a19b4f2e5d1 --- /dev/null +++ b/tests/wpt/meta/dom/nodes/moveBefore/tentative/css-transition-cross-shadow.html.ini @@ -0,0 +1,3 @@ +[css-transition-cross-shadow.html] + [Moving an element with a transition across shadow boundaries should reset the transition] + expected: FAIL diff --git a/tests/wpt/meta/dom/nodes/moveBefore/tentative/css-transition-to-disconnected-document.html.ini b/tests/wpt/meta/dom/nodes/moveBefore/tentative/css-transition-to-disconnected-document.html.ini new file mode 100644 index 00000000000..dd6192bc1b6 --- /dev/null +++ b/tests/wpt/meta/dom/nodes/moveBefore/tentative/css-transition-to-disconnected-document.html.ini @@ -0,0 +1,3 @@ +[css-transition-to-disconnected-document.html] + [Moving an element with a transition to a disconnected document should reset the transitionm state] + expected: FAIL diff --git a/tests/wpt/meta/dom/nodes/moveBefore/tentative/css-transition-trigger.html.ini b/tests/wpt/meta/dom/nodes/moveBefore/tentative/css-transition-trigger.html.ini new file mode 100644 index 00000000000..272163fb327 --- /dev/null +++ b/tests/wpt/meta/dom/nodes/moveBefore/tentative/css-transition-trigger.html.ini @@ -0,0 +1,3 @@ +[css-transition-trigger.html] + [Node.moveBefore should trigger CSS transition state (left) if needed] + expected: FAIL diff --git a/tests/wpt/meta/dom/observable/tentative/observable-every.any.js.ini b/tests/wpt/meta/dom/observable/tentative/observable-every.any.js.ini new file mode 100644 index 00000000000..9c2fa7172f5 --- /dev/null +++ b/tests/wpt/meta/dom/observable/tentative/observable-every.any.js.ini @@ -0,0 +1,62 @@ +[observable-every.any.worker.html] + [every(): Promise resolves to true if all values pass the predicate] + expected: FAIL + + [every(): Promise resolves to false if any value fails the predicate] + expected: FAIL + + [every(): Abort the subscription to the source if the predicate does not pass] + expected: FAIL + + [every(): Lifecycle checks when all values pass the predicate] + expected: FAIL + + [every(): Lifecycle checks when any value fails the predicate] + expected: FAIL + + [every(): Resolves with true if the observable completes without emitting a value] + expected: FAIL + + [every(): Rejects with any error emitted from the source observable] + expected: FAIL + + [every(): Rejects with any error thrown from the predicate] + expected: FAIL + + [every(): Index is passed into the predicate] + expected: FAIL + + [every(): Rejects with a DOMException if the source Observable is aborted] + expected: FAIL + + +[observable-every.any.html] + [every(): Promise resolves to true if all values pass the predicate] + expected: FAIL + + [every(): Promise resolves to false if any value fails the predicate] + expected: FAIL + + [every(): Abort the subscription to the source if the predicate does not pass] + expected: FAIL + + [every(): Lifecycle checks when all values pass the predicate] + expected: FAIL + + [every(): Lifecycle checks when any value fails the predicate] + expected: FAIL + + [every(): Resolves with true if the observable completes without emitting a value] + expected: FAIL + + [every(): Rejects with any error emitted from the source observable] + expected: FAIL + + [every(): Rejects with any error thrown from the predicate] + expected: FAIL + + [every(): Index is passed into the predicate] + expected: FAIL + + [every(): Rejects with a DOMException if the source Observable is aborted] + expected: FAIL diff --git a/tests/wpt/meta/dom/observable/tentative/observable-filter.any.js.ini b/tests/wpt/meta/dom/observable/tentative/observable-filter.any.js.ini index 159169b6565..64a04893c3b 100644 --- a/tests/wpt/meta/dom/observable/tentative/observable-filter.any.js.ini +++ b/tests/wpt/meta/dom/observable/tentative/observable-filter.any.js.ini @@ -14,6 +14,9 @@ [filter(): Upon source completion, source Observable teardown sequence happens after downstream filter complete() is called] expected: FAIL + [filter(): Index is passed correctly to predicate] + expected: FAIL + [observable-filter.any.worker.html] [filter(): Returned Observable filters out results based on predicate] @@ -30,3 +33,6 @@ [filter(): Upon source completion, source Observable teardown sequence happens after downstream filter complete() is called] expected: FAIL + + [filter(): Index is passed correctly to predicate] + expected: FAIL diff --git a/tests/wpt/meta/dom/observable/tentative/observable-find.any.js.ini b/tests/wpt/meta/dom/observable/tentative/observable-find.any.js.ini new file mode 100644 index 00000000000..9124f4ad973 --- /dev/null +++ b/tests/wpt/meta/dom/observable/tentative/observable-find.any.js.ini @@ -0,0 +1,38 @@ +[observable-find.any.html] + [find(): Promise resolves with the first value that passes the predicate] + expected: FAIL + + [find(): Promise resolves with undefined if no value passes the predicate] + expected: FAIL + + [find(): Promise rejects with the error emitted from the source Observable] + expected: FAIL + + [find(): Promise rejects with any error thrown from the predicate] + expected: FAIL + + [find(): Passes the index of the value to the predicate] + expected: FAIL + + [find(): Rejects with AbortError when the signal is aborted] + expected: FAIL + + +[observable-find.any.worker.html] + [find(): Promise resolves with the first value that passes the predicate] + expected: FAIL + + [find(): Promise resolves with undefined if no value passes the predicate] + expected: FAIL + + [find(): Promise rejects with the error emitted from the source Observable] + expected: FAIL + + [find(): Promise rejects with any error thrown from the predicate] + expected: FAIL + + [find(): Passes the index of the value to the predicate] + expected: FAIL + + [find(): Rejects with AbortError when the signal is aborted] + expected: FAIL diff --git a/tests/wpt/meta/dom/observable/tentative/observable-inspect.any.js.ini b/tests/wpt/meta/dom/observable/tentative/observable-inspect.any.js.ini new file mode 100644 index 00000000000..8f4dceede55 --- /dev/null +++ b/tests/wpt/meta/dom/observable/tentative/observable-inspect.any.js.ini @@ -0,0 +1,80 @@ +[observable-inspect.any.html] + [inspect(): Provides a pre-subscription subscribe callback] + expected: FAIL + + [inspect(): Provides a way to tap into the values and completions of the source observable using an observer] + expected: FAIL + + [inspect(): Error handler does not stop error from being reported to the global, when subscriber does not pass error handler] + expected: FAIL + + [inspect(): Provides a way to tap into the values and errors of the source observable using an observer. Errors are passed through] + expected: FAIL + + [inspect(): ObserverCallback passed in] + expected: FAIL + + [inspect(): Throwing an error in the observer next handler is caught and sent to the error callback of the result observable] + expected: FAIL + + [inspect(): Throwing an error in the observer error handler in inspect() is caught and sent to the error callback of the result observable] + expected: FAIL + + [inspect(): Throwing an error in the observer complete handler is caught and sent to the error callback of the result observable] + expected: FAIL + + [inspect(): Throwing an error in the next handler function in do should be caught and sent to the error callback of the result observable] + expected: FAIL + + [inspect(): Errors thrown in subscribe() Inspector handler subscribe handler are caught and sent to error callback] + expected: FAIL + + [inspect(): Provides a way to tap into the moment a source observable is unsubscribed from] + expected: FAIL + + [inspect(): Inspector abort() handler is not called if the source completes before the result is unsubscribed from] + expected: FAIL + + [inspect(): Errors thrown from inspect()'s abort() handler are caught and reported to the global, because the subscription is already closed by the time the handler runs] + expected: FAIL + + +[observable-inspect.any.worker.html] + [inspect(): Provides a pre-subscription subscribe callback] + expected: FAIL + + [inspect(): Provides a way to tap into the values and completions of the source observable using an observer] + expected: FAIL + + [inspect(): Error handler does not stop error from being reported to the global, when subscriber does not pass error handler] + expected: FAIL + + [inspect(): Provides a way to tap into the values and errors of the source observable using an observer. Errors are passed through] + expected: FAIL + + [inspect(): ObserverCallback passed in] + expected: FAIL + + [inspect(): Throwing an error in the observer next handler is caught and sent to the error callback of the result observable] + expected: FAIL + + [inspect(): Throwing an error in the observer error handler in inspect() is caught and sent to the error callback of the result observable] + expected: FAIL + + [inspect(): Throwing an error in the observer complete handler is caught and sent to the error callback of the result observable] + expected: FAIL + + [inspect(): Throwing an error in the next handler function in do should be caught and sent to the error callback of the result observable] + expected: FAIL + + [inspect(): Errors thrown in subscribe() Inspector handler subscribe handler are caught and sent to error callback] + expected: FAIL + + [inspect(): Provides a way to tap into the moment a source observable is unsubscribed from] + expected: FAIL + + [inspect(): Inspector abort() handler is not called if the source completes before the result is unsubscribed from] + expected: FAIL + + [inspect(): Errors thrown from inspect()'s abort() handler are caught and reported to the global, because the subscription is already closed by the time the handler runs] + expected: FAIL diff --git a/tests/wpt/meta/dom/observable/tentative/observable-some.any.js.ini b/tests/wpt/meta/dom/observable/tentative/observable-some.any.js.ini new file mode 100644 index 00000000000..d02c831bf39 --- /dev/null +++ b/tests/wpt/meta/dom/observable/tentative/observable-some.any.js.ini @@ -0,0 +1,44 @@ +[observable-some.any.worker.html] + [some(): subscriber is inactive after the first value that passes the predicate, because the source was unsubscribed from] + expected: FAIL + + [observable-some] + expected: FAIL + + [observable-some 1] + expected: FAIL + + [some(): The returned promise rejects with an error if the predicate errors] + expected: FAIL + + [some(): The returned promise rejects with an error if the source observable errors] + expected: FAIL + + [some(): The returned promise resolves as false if the source observable completes without emitting a value] + expected: FAIL + + [some(): The return promise rejects with a DOMException if the signal is aborted] + expected: FAIL + + +[observable-some.any.html] + [some(): subscriber is inactive after the first value that passes the predicate, because the source was unsubscribed from] + expected: FAIL + + [observable-some] + expected: FAIL + + [observable-some 1] + expected: FAIL + + [some(): The returned promise rejects with an error if the predicate errors] + expected: FAIL + + [some(): The returned promise rejects with an error if the source observable errors] + expected: FAIL + + [some(): The returned promise resolves as false if the source observable completes without emitting a value] + expected: FAIL + + [some(): The return promise rejects with a DOMException if the signal is aborted] + expected: FAIL diff --git a/tests/wpt/meta/fetch/api/request/request-bad-port.any.js.ini b/tests/wpt/meta/fetch/api/request/request-bad-port.any.js.ini index 915a01c0a69..99d17bcf9c2 100644 --- a/tests/wpt/meta/fetch/api/request/request-bad-port.any.js.ini +++ b/tests/wpt/meta/fetch/api/request/request-bad-port.any.js.ini @@ -2,148 +2,8 @@ expected: ERROR [request-bad-port.any.html] - expected: TIMEOUT - [Request on bad port 6697 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 3659 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 4045 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 1720 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6665 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6000 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 1723 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6666 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6667 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6668 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6669 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 2049 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 5061 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 5060 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 989 should throw TypeError.] - expected: TIMEOUT - - [Request on bad port 990 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 993 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 995 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 1719 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6566 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 10080 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 4190 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6679 should throw TypeError.] - expected: NOTRUN - [request-bad-port.any.worker.html] - expected: TIMEOUT - [Request on bad port 6697 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 3659 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 4045 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 1720 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6665 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6000 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 1723 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6666 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6667 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6668 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6669 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 2049 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 5061 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 5060 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 989 should throw TypeError.] - expected: TIMEOUT - - [Request on bad port 990 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 993 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 995 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 1719 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6566 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 10080 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 4190 should throw TypeError.] - expected: NOTRUN - - [Request on bad port 6679 should throw TypeError.] - expected: NOTRUN - [request-bad-port.any.sharedworker.html] expected: ERROR diff --git a/tests/wpt/meta/fetch/metadata/generated/appcache-manifest.https.sub.html.ini b/tests/wpt/meta/fetch/metadata/generated/appcache-manifest.https.sub.html.ini deleted file mode 100644 index fdf575f99df..00000000000 --- a/tests/wpt/meta/fetch/metadata/generated/appcache-manifest.https.sub.html.ini +++ /dev/null @@ -1,60 +0,0 @@ -[appcache-manifest.https.sub.html] - [sec-fetch-site - Same origin] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Cross-site] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Same site] - expected: PRECONDITION_FAILED - - [sec-fetch-site - HTTPS downgrade (header not sent)] - expected: PRECONDITION_FAILED - - [sec-fetch-site - HTTPS upgrade] - expected: PRECONDITION_FAILED - - [sec-fetch-site - HTTPS downgrade-upgrade] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Cross-Site -> Same Origin] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Cross-Site -> Same-Site] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Cross-Site -> Cross-Site] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Same-Origin -> Same Origin] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Same-Origin -> Same-Site] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Same-Origin -> Cross-Site] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Same-Site -> Same Origin] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Same-Site -> Same-Site] - expected: PRECONDITION_FAILED - - [sec-fetch-site - Same-Site -> Cross-Site] - expected: PRECONDITION_FAILED - - [sec-fetch-mode] - expected: PRECONDITION_FAILED - - [sec-fetch-dest] - expected: PRECONDITION_FAILED - - [sec-fetch-user] - expected: PRECONDITION_FAILED diff --git a/tests/wpt/meta/fetch/metadata/generated/css-images.sub.tentative.html.ini b/tests/wpt/meta/fetch/metadata/generated/css-images.sub.tentative.html.ini index 4b5c3e26586..7b4cf2c5ae7 100644 --- a/tests/wpt/meta/fetch/metadata/generated/css-images.sub.tentative.html.ini +++ b/tests/wpt/meta/fetch/metadata/generated/css-images.sub.tentative.html.ini @@ -185,3 +185,6 @@ [border-image sec-fetch-site - HTTPS downgrade (header not sent)] expected: FAIL + + [background-image sec-fetch-site - HTTPS downgrade (header not sent)] + expected: TIMEOUT diff --git a/tests/wpt/meta/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-pageshow-events-iframe-contentWindow.html.ini b/tests/wpt/meta/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-pageshow-events-iframe-contentWindow.html.ini index 149bcb4ff8c..b8fd22e2b81 100644 --- a/tests/wpt/meta/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-pageshow-events-iframe-contentWindow.html.ini +++ b/tests/wpt/meta/html/browsers/browsing-the-web/navigating-across-documents/initial-empty-document/load-pageshow-events-iframe-contentWindow.html.ini @@ -10,3 +10,6 @@ [load & pageshow events do not fire on contentWindow of <iframe> element created with src='about:blank'] expected: FAIL + + [load & pageshow events do not fire on contentWindow of <iframe> element created with src=''] + expected: FAIL diff --git a/tests/wpt/meta/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-referrer.window.js.ini b/tests/wpt/meta/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-referrer.window.js.ini index 5a245d61359..bcda478adb1 100644 --- a/tests/wpt/meta/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-referrer.window.js.ini +++ b/tests/wpt/meta/html/browsers/browsing-the-web/navigating-across-documents/javascript-url-referrer.window.js.ini @@ -1,6 +1,10 @@ [javascript-url-referrer.window.html] + expected: TIMEOUT [unsafe-url referrer policy used to create the starting page] expected: FAIL [origin referrer policy used to create the starting page] expected: FAIL + + [no-referrer referrer policy used to create the starting page] + expected: TIMEOUT diff --git a/tests/wpt/meta/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-click.html.ini b/tests/wpt/meta/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-click.html.ini new file mode 100644 index 00000000000..60a4fa51f8a --- /dev/null +++ b/tests/wpt/meta/html/browsers/browsing-the-web/navigating-across-documents/replace-before-load/a-click.html.ini @@ -0,0 +1,3 @@ +[a-click.html] + [aElement.click() before the load event must NOT replace] + expected: FAIL diff --git a/tests/wpt/meta/html/browsers/history/the-history-interface/traverse_the_history_3.html.ini b/tests/wpt/meta/html/browsers/history/the-history-interface/traverse_the_history_3.html.ini deleted file mode 100644 index a03a8322165..00000000000 --- a/tests/wpt/meta/html/browsers/history/the-history-interface/traverse_the_history_3.html.ini +++ /dev/null @@ -1,3 +0,0 @@ -[traverse_the_history_3.html] - [Multiple history traversals, last would be aborted] - expected: FAIL diff --git a/tests/wpt/meta-legacy-layout/html/browsers/history/the-history-interface/traverse_the_history_2.html.ini b/tests/wpt/meta/html/browsers/history/the-history-interface/traverse_the_history_4.html.ini index 5d17a8e9419..d6188c03424 100644 --- a/tests/wpt/meta-legacy-layout/html/browsers/history/the-history-interface/traverse_the_history_2.html.ini +++ b/tests/wpt/meta/html/browsers/history/the-history-interface/traverse_the_history_4.html.ini @@ -1,3 +1,3 @@ -[traverse_the_history_2.html] +[traverse_the_history_4.html] [Multiple history traversals, last would be aborted] expected: FAIL diff --git a/tests/wpt/meta/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.html.ini b/tests/wpt/meta/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.html.ini new file mode 100644 index 00000000000..fea0e7cbc96 --- /dev/null +++ b/tests/wpt/meta/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.html.ini @@ -0,0 +1,3 @@ +[2d.fillStyle.parse.hsl-clamp-negative-saturation.html] + [OffscreenCanvas test: 2d.fillStyle.parse.hsl-clamp-negative-saturation] + expected: FAIL diff --git a/tests/wpt/meta/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.worker.js.ini b/tests/wpt/meta/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.worker.js.ini new file mode 100644 index 00000000000..47b3d01d95f --- /dev/null +++ b/tests/wpt/meta/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.worker.js.ini @@ -0,0 +1,3 @@ +[2d.fillStyle.parse.hsl-clamp-negative-saturation.worker.html] + [2d] + expected: FAIL diff --git a/tests/wpt/meta/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.html.ini b/tests/wpt/meta/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.html.ini new file mode 100644 index 00000000000..5540951314e --- /dev/null +++ b/tests/wpt/meta/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.html.ini @@ -0,0 +1,3 @@ +[2d.fillStyle.parse.hsla-clamp-alpha-1.html] + [OffscreenCanvas test: 2d.fillStyle.parse.hsla-clamp-alpha-1] + expected: FAIL diff --git a/tests/wpt/meta/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.worker.js.ini b/tests/wpt/meta/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.worker.js.ini new file mode 100644 index 00000000000..bcfbd26b626 --- /dev/null +++ b/tests/wpt/meta/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.worker.js.ini @@ -0,0 +1,3 @@ +[2d.fillStyle.parse.hsla-clamp-alpha-1.worker.html] + [2d] + expected: FAIL diff --git a/tests/wpt/meta/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.html.ini b/tests/wpt/meta/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.html.ini new file mode 100644 index 00000000000..365d9f23d3f --- /dev/null +++ b/tests/wpt/meta/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.html.ini @@ -0,0 +1,3 @@ +[2d.fillStyle.parse.hsla-clamp-alpha-2.html] + [OffscreenCanvas test: 2d.fillStyle.parse.hsla-clamp-alpha-2] + expected: FAIL diff --git a/tests/wpt/meta/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.worker.js.ini b/tests/wpt/meta/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.worker.js.ini new file mode 100644 index 00000000000..9a23a66e687 --- /dev/null +++ b/tests/wpt/meta/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.worker.js.ini @@ -0,0 +1,3 @@ +[2d.fillStyle.parse.hsla-clamp-alpha-2.worker.html] + [2d] + expected: FAIL diff --git a/tests/wpt/meta/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.html.ini b/tests/wpt/meta/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.html.ini new file mode 100644 index 00000000000..995bfd3bb8c --- /dev/null +++ b/tests/wpt/meta/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.html.ini @@ -0,0 +1,3 @@ +[2d.fillStyle.parse.hsla-clamp-negative-saturation.html] + [OffscreenCanvas test: 2d.fillStyle.parse.hsla-clamp-negative-saturation] + expected: FAIL diff --git a/tests/wpt/meta/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.worker.js.ini b/tests/wpt/meta/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.worker.js.ini new file mode 100644 index 00000000000..35507496f1b --- /dev/null +++ b/tests/wpt/meta/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.worker.js.ini @@ -0,0 +1,3 @@ +[2d.fillStyle.parse.hsla-clamp-negative-saturation.worker.html] + [2d] + expected: FAIL diff --git a/tests/wpt/meta/html/dom/idlharness.https.html.ini b/tests/wpt/meta/html/dom/idlharness.https.html.ini index 34981e3b785..05eb99aed01 100644 --- a/tests/wpt/meta/html/dom/idlharness.https.html.ini +++ b/tests/wpt/meta/html/dom/idlharness.https.html.ini @@ -1973,6 +1973,21 @@ [ElementInternals interface: attribute states] expected: FAIL + [Element interface: document.createElement("noscript") must inherit property "getHTML(optional GetHTMLOptions)" with the proper type] + expected: FAIL + + [Element interface: calling getHTML(optional GetHTMLOptions) on document.createElement("noscript") with too few arguments must throw TypeError] + expected: FAIL + + [ShadowRoot interface: operation getHTML(optional GetHTMLOptions)] + expected: FAIL + + [ShadowRoot interface: attribute innerHTML] + expected: FAIL + + [Element interface: operation getHTML(optional GetHTMLOptions)] + expected: FAIL + [idlharness.https.html?include=(Document|Window)] [Document interface: documentWithHandlers must inherit property "queryCommandEnabled(DOMString)" with the proper type] @@ -4811,3 +4826,9 @@ [HTMLElement interface: document.createElement("noscript") must inherit property "writingSuggestions" with the proper type] expected: FAIL + + [HTMLTemplateElement interface: attribute shadowRootSerializable] + expected: FAIL + + [HTMLTemplateElement interface: document.createElement("template") must inherit property "shadowRootSerializable" with the proper type] + expected: FAIL diff --git a/tests/wpt/meta/html/infrastructure/urls/base-url/document-base-url-window-initiator-is-not-opener.https.window.js.ini b/tests/wpt/meta/html/infrastructure/urls/base-url/document-base-url-window-initiator-is-not-opener.https.window.js.ini index 2ef0896e3b3..8b8af2b9c2e 100644 --- a/tests/wpt/meta/html/infrastructure/urls/base-url/document-base-url-window-initiator-is-not-opener.https.window.js.ini +++ b/tests/wpt/meta/html/infrastructure/urls/base-url/document-base-url-window-initiator-is-not-opener.https.window.js.ini @@ -1,4 +1,3 @@ [document-base-url-window-initiator-is-not-opener.https.window.html] - expected: TIMEOUT [window.open() gets base url from initiator not opener.] expected: [FAIL, PASS, TIMEOUT] diff --git a/tests/wpt/meta/html/semantics/embedded-content/media-elements/track/track-element/no-cuechange-before-play.html.ini b/tests/wpt/meta/html/semantics/embedded-content/media-elements/track/track-element/no-cuechange-before-play.html.ini index 246e55deb7e..4caf5931a11 100644 --- a/tests/wpt/meta/html/semantics/embedded-content/media-elements/track/track-element/no-cuechange-before-play.html.ini +++ b/tests/wpt/meta/html/semantics/embedded-content/media-elements/track/track-element/no-cuechange-before-play.html.ini @@ -1,4 +1,3 @@ [no-cuechange-before-play.html] - expected: TIMEOUT [Ensure that the 'cuechange' event is not fired before video playback has begun.] - expected: TIMEOUT + expected: FAIL diff --git a/tests/wpt/meta/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-3.html.ini b/tests/wpt/meta/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-3.html.ini index 6a420504feb..d4b2e4435a0 100644 --- a/tests/wpt/meta/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-3.html.ini +++ b/tests/wpt/meta/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_escaping-3.html.ini @@ -1,3 +1,3 @@ [iframe_sandbox_popups_escaping-3.html] [Check that popups from a sandboxed iframe escape the sandbox if\n allow-popups-to-escape-sandbox is used] - expected: FAIL + expected: TIMEOUT diff --git a/tests/wpt/meta/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_nonescaping-2.html.ini b/tests/wpt/meta/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_nonescaping-2.html.ini index c6f1e5d7d84..a6591b318dc 100644 --- a/tests/wpt/meta/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_nonescaping-2.html.ini +++ b/tests/wpt/meta/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_nonescaping-2.html.ini @@ -1,4 +1,4 @@ [iframe_sandbox_popups_nonescaping-2.html] expected: TIMEOUT [Check that popups from a sandboxed iframe do not escape the sandbox] - expected: NOTRUN + expected: FAIL diff --git a/tests/wpt/meta/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_nonescaping-3.html.ini b/tests/wpt/meta/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_nonescaping-3.html.ini index d5fd800f09d..d89a1691435 100644 --- a/tests/wpt/meta/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_nonescaping-3.html.ini +++ b/tests/wpt/meta/html/semantics/embedded-content/the-iframe-element/iframe_sandbox_popups_nonescaping-3.html.ini @@ -1,4 +1,4 @@ [iframe_sandbox_popups_nonescaping-3.html] expected: TIMEOUT [Check that popups from a sandboxed iframe do not escape the sandbox] - expected: NOTRUN + expected: FAIL diff --git a/tests/wpt/meta/html/semantics/embedded-content/the-img-element/sizes/parse-a-sizes-attribute-width-1000px.html.ini b/tests/wpt/meta/html/semantics/embedded-content/the-img-element/sizes/parse-a-sizes-attribute-width-1000px.html.ini new file mode 100644 index 00000000000..b6fea6b42c7 --- /dev/null +++ b/tests/wpt/meta/html/semantics/embedded-content/the-img-element/sizes/parse-a-sizes-attribute-width-1000px.html.ini @@ -0,0 +1,3 @@ +[parse-a-sizes-attribute-width-1000px.html] + [<img srcset="/images/green-1x1.png?e38 50w, /images/green-16x16.png?e38 51w" sizes="(min-width:calc(0)) 1px"> ref sizes="1px" (width:1000px)] + expected: FAIL diff --git a/tests/wpt/meta/html/semantics/forms/form-submission-0/reparent-form-during-planned-navigation-task.html.ini b/tests/wpt/meta/html/semantics/forms/form-submission-0/reparent-form-during-planned-navigation-task.html.ini deleted file mode 100644 index 7682a4830bf..00000000000 --- a/tests/wpt/meta/html/semantics/forms/form-submission-0/reparent-form-during-planned-navigation-task.html.ini +++ /dev/null @@ -1,4 +0,0 @@ -[reparent-form-during-planned-navigation-task.html] - expected: TIMEOUT - [reparent-form-during-planned-navigation-task] - expected: TIMEOUT diff --git a/tests/wpt/meta/html/semantics/forms/form-submission-0/urlencoded2.window.js.ini b/tests/wpt/meta/html/semantics/forms/form-submission-0/urlencoded2.window.js.ini index 86ba7d93f9d..b285151857b 100644 --- a/tests/wpt/meta/html/semantics/forms/form-submission-0/urlencoded2.window.js.ini +++ b/tests/wpt/meta/html/semantics/forms/form-submission-0/urlencoded2.window.js.ini @@ -181,3 +181,6 @@ [application/x-www-form-urlencoded: \\r\\n in name (formdata event)] expected: FAIL + + [application/x-www-form-urlencoded: 0x00 in name (normal form)] + expected: FAIL diff --git a/tests/wpt/meta/html/semantics/forms/the-input-element/input-stepdown-02.html.ini b/tests/wpt/meta/html/semantics/forms/the-input-element/input-stepdown-02.html.ini new file mode 100644 index 00000000000..4df29982fe6 --- /dev/null +++ b/tests/wpt/meta/html/semantics/forms/the-input-element/input-stepdown-02.html.ini @@ -0,0 +1,3 @@ +[input-stepdown-02.html] + [stepDown() on input with no initial value and positive min value] + expected: FAIL diff --git a/tests/wpt/meta/html/semantics/permission-element/bounded-sizes-reftest.tentative.html.ini b/tests/wpt/meta/html/semantics/permission-element/bounded-sizes-reftest.tentative.html.ini new file mode 100644 index 00000000000..d4757fa0c17 --- /dev/null +++ b/tests/wpt/meta/html/semantics/permission-element/bounded-sizes-reftest.tentative.html.ini @@ -0,0 +1,2 @@ +[bounded-sizes-reftest.tentative.html] + expected: FAIL diff --git a/tests/wpt/meta/mediasession/idlharness.window.js.ini b/tests/wpt/meta/mediasession/idlharness.window.js.ini index 04906d19d00..d0edde1c114 100644 --- a/tests/wpt/meta/mediasession/idlharness.window.js.ini +++ b/tests/wpt/meta/mediasession/idlharness.window.js.ini @@ -22,3 +22,36 @@ [MediaMetadata interface: new MediaMetadata() must inherit property "artwork" with the proper type] expected: FAIL + + [MediaMetadata interface: attribute chapterInfo] + expected: FAIL + + [MediaMetadata interface: new MediaMetadata() must inherit property "chapterInfo" with the proper type] + expected: FAIL + + [ChapterInformation interface: existence and properties of interface object] + expected: FAIL + + [ChapterInformation interface object length] + expected: FAIL + + [ChapterInformation interface object name] + expected: FAIL + + [ChapterInformation interface: existence and properties of interface prototype object] + expected: FAIL + + [ChapterInformation interface: existence and properties of interface prototype object's "constructor" property] + expected: FAIL + + [ChapterInformation interface: existence and properties of interface prototype object's @@unscopables property] + expected: FAIL + + [ChapterInformation interface: attribute title] + expected: FAIL + + [ChapterInformation interface: attribute startTime] + expected: FAIL + + [ChapterInformation interface: attribute artwork] + expected: FAIL diff --git a/tests/wpt/meta/streams/readable-streams/async-iterator.any.js.ini b/tests/wpt/meta/streams/readable-streams/async-iterator.any.js.ini index 840d5e2d798..d2a39d72425 100644 --- a/tests/wpt/meta/streams/readable-streams/async-iterator.any.js.ini +++ b/tests/wpt/meta/streams/readable-streams/async-iterator.any.js.ini @@ -122,6 +122,12 @@ [close() while next() is pending] expected: FAIL + [return(); next() with delayed cancel()] + expected: FAIL + + [return(); next() with delayed cancel() [no awaiting\]] + expected: FAIL + [async-iterator.any.worker.html] [return() rejects if the stream has errored] @@ -241,6 +247,12 @@ [close() while next() is pending] expected: FAIL + [return(); next() with delayed cancel()] + expected: FAIL + + [return(); next() with delayed cancel() [no awaiting\]] + expected: FAIL + [async-iterator.any.shadowrealm.html] expected: ERROR diff --git a/tests/wpt/meta/wasm/jsapi/idlharness.any.js.ini b/tests/wpt/meta/wasm/jsapi/idlharness.any.js.ini index c1decaa7708..5702c4b097b 100644 --- a/tests/wpt/meta/wasm/jsapi/idlharness.any.js.ini +++ b/tests/wpt/meta/wasm/jsapi/idlharness.any.js.ini @@ -2,7 +2,31 @@ [Table interface: operation set(unsigned long, optional any)] expected: FAIL + [Memory interface: operation toFixedLengthBuffer()] + expected: FAIL + + [Memory interface: operation toResizableBuffer()] + expected: FAIL + + [Memory interface: [object WebAssembly.Memory\] must inherit property "toFixedLengthBuffer()" with the proper type] + expected: FAIL + + [Memory interface: [object WebAssembly.Memory\] must inherit property "toResizableBuffer()" with the proper type] + expected: FAIL + [idlharness.any.worker.html] [Table interface: operation set(unsigned long, optional any)] expected: FAIL + + [Memory interface: operation toFixedLengthBuffer()] + expected: FAIL + + [Memory interface: operation toResizableBuffer()] + expected: FAIL + + [Memory interface: [object WebAssembly.Memory\] must inherit property "toFixedLengthBuffer()" with the proper type] + expected: FAIL + + [Memory interface: [object WebAssembly.Memory\] must inherit property "toResizableBuffer()" with the proper type] + expected: FAIL diff --git a/tests/wpt/meta/webidl/ecmascript-binding/legacy-factory-function-builtin-properties.window.js.ini b/tests/wpt/meta/webidl/ecmascript-binding/legacy-factory-function-builtin-properties.window.js.ini new file mode 100644 index 00000000000..c1c0e6b9043 --- /dev/null +++ b/tests/wpt/meta/webidl/ecmascript-binding/legacy-factory-function-builtin-properties.window.js.ini @@ -0,0 +1,3 @@ +[legacy-factory-function-builtin-properties.window.html] + [Legacy factory function property enumeration order of "length", "name", and "prototype"] + expected: FAIL diff --git a/tests/wpt/meta/webmessaging/with-ports/018.html.ini b/tests/wpt/meta/webmessaging/with-ports/018.html.ini deleted file mode 100644 index b7b36c1d3a4..00000000000 --- a/tests/wpt/meta/webmessaging/with-ports/018.html.ini +++ /dev/null @@ -1,4 +0,0 @@ -[018.html] - expected: TIMEOUT - [origin of the script that invoked the method, javascript:] - expected: TIMEOUT diff --git a/tests/wpt/meta/webmessaging/without-ports/017.html.ini b/tests/wpt/meta/webmessaging/without-ports/017.html.ini deleted file mode 100644 index c7946fc91b4..00000000000 --- a/tests/wpt/meta/webmessaging/without-ports/017.html.ini +++ /dev/null @@ -1,4 +0,0 @@ -[017.html] - expected: TIMEOUT - [origin of the script that invoked the method, about:blank] - expected: TIMEOUT diff --git a/tests/wpt/meta/webmessaging/without-ports/018.html.ini b/tests/wpt/meta/webmessaging/without-ports/018.html.ini deleted file mode 100644 index b7b36c1d3a4..00000000000 --- a/tests/wpt/meta/webmessaging/without-ports/018.html.ini +++ /dev/null @@ -1,4 +0,0 @@ -[018.html] - expected: TIMEOUT - [origin of the script that invoked the method, javascript:] - expected: TIMEOUT diff --git a/tests/wpt/meta/webxr/idlharness.https.window.js.ini b/tests/wpt/meta/webxr/idlharness.https.window.js.ini index b6ca19f818c..6ddabb68fd2 100644 --- a/tests/wpt/meta/webxr/idlharness.https.window.js.ini +++ b/tests/wpt/meta/webxr/idlharness.https.window.js.ini @@ -361,3 +361,6 @@ [XRInputSourcesChangeEvent interface: attribute removed] expected: FAIL + + [XRInputSource interface: attribute skipRendering] + expected: FAIL diff --git a/tests/wpt/tests/.github/workflows/interfaces.yml b/tests/wpt/tests/.github/workflows/interfaces.yml index 04636e0bb3d..ff2a679b598 100644 --- a/tests/wpt/tests/.github/workflows/interfaces.yml +++ b/tests/wpt/tests/.github/workflows/interfaces.yml @@ -15,7 +15,7 @@ jobs: - name: Create pull request # Use a conditional step instead of a conditional job to work around #20700. if: github.repository == 'web-platform-tests/wpt' - uses: peter-evans/create-pull-request@v5 + uses: peter-evans/create-pull-request@v6 with: token: ${{ secrets.GITHUB_TOKEN }} author: wpt-pr-bot <wpt-pr-bot@users.noreply.github.com> diff --git a/tests/wpt/tests/.github/workflows/regen_certs.yml b/tests/wpt/tests/.github/workflows/regen_certs.yml index d3e1240e79a..634ac202d5f 100644 --- a/tests/wpt/tests/.github/workflows/regen_certs.yml +++ b/tests/wpt/tests/.github/workflows/regen_certs.yml @@ -24,7 +24,7 @@ jobs: - name: Commit and create pull request # Use a conditional step instead of a conditional job to work around #20700. if: github.repository == 'web-platform-tests/wpt' - uses: peter-evans/create-pull-request@v5 + uses: peter-evans/create-pull-request@v6 with: token: ${{ secrets.GITHUB_TOKEN }} author: wpt-pr-bot <wpt-pr-bot@users.noreply.github.com> diff --git a/tests/wpt/tests/accname/name/comp_name_from_content.html b/tests/wpt/tests/accname/name/comp_name_from_content.html index 3504658ea46..2f6b3ab6b47 100644 --- a/tests/wpt/tests/accname/name/comp_name_from_content.html +++ b/tests/wpt/tests/accname/name/comp_name_from_content.html @@ -239,7 +239,7 @@ <span class="note" id="crossref_link">link</span><!-- this text is skipped the first time around because of aria-labelledby on parent element --> </a> <!-- but it's picked up again in inverse order b/c of cross-referencial aria-labelledby edge case --> - <img id="nested_image_label_3" alt="image" aria-labelledby="crossref_link" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="> + <img id="nested_image_label3" alt="image" aria-labelledby="crossref_link" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="> </h3> <!-- self-referencial edge case--> diff --git a/tests/wpt/tests/accname/name/comp_text_node.html b/tests/wpt/tests/accname/name/comp_text_node.html index a31f9e02451..f9cb8f1baf8 100644 --- a/tests/wpt/tests/accname/name/comp_text_node.html +++ b/tests/wpt/tests/accname/name/comp_text_node.html @@ -101,11 +101,11 @@ <br> <h1>text node, with leading/trailing non-breaking space</h1> -<span role="button" tabindex="0" class="ex" data-expectedlabel="button label" data-testname="span[role=button] with text node, with leading/trailing non-breaking space"> button label </span> -<div role="heading" class="ex" data-expectedlabel="heading label" data-testname="div[role=heading] with text node, with leading/trailing non-breaking space"> heading label </div> -<button class="ex" data-expectedlabel="button label" data-testname="button with text node, with leading/trailing non-breaking space"> button label </button> -<h3 class="ex" data-expectedlabel="heading label" data-testname="heading with text node, with leading/trailing non-breaking space"> heading label </h3> -<a href="#" class="ex" data-expectedlabel="link label" data-testname="link with text node, with leading/trailing non-breaking space"> link label </a> +<span role="button" tabindex="0" class="ex" data-expectedlabel=" button label " data-testname="span[role=button] with text node, with leading/trailing non-breaking space"> button label </span> +<div role="heading" class="ex" data-expectedlabel=" heading label " data-testname="div[role=heading] with text node, with leading/trailing non-breaking space"> heading label </div> +<button class="ex" data-expectedlabel=" button label " data-testname="button with text node, with leading/trailing non-breaking space"> button label </button> +<h3 class="ex" data-expectedlabel=" heading label " data-testname="heading with text node, with leading/trailing non-breaking space"> heading label </h3> +<a href="#" class="ex" data-expectedlabel=" link label " data-testname="link with text node, with leading/trailing non-breaking space"> link label </a> <br> <h1>text node, with mixed space and non-breaking space</h1> diff --git a/tests/wpt/tests/close-watcher/abortsignal.html b/tests/wpt/tests/close-watcher/abortsignal.html index 9229b37cf66..ec360f483d4 100644 --- a/tests/wpt/tests/close-watcher/abortsignal.html +++ b/tests/wpt/tests/close-watcher/abortsignal.html @@ -49,7 +49,7 @@ test(() => { watcher.requestClose(); controller.abort(); - assert_equals(oncancel_call_count_, 0); + assert_equals(oncancel_call_count_, 1); assert_equals(onclose_call_count_, 1); }, "requestClose() then abortController.abort() fires only one close event"); @@ -92,7 +92,7 @@ promise_test(async t => { await sendCloseRequest(); controller.abort(); - assert_equals(oncancel_call_count_, 0); + assert_equals(oncancel_call_count_, 1); assert_equals(onclose_call_count_, 1); }, "Esc key then abortController.abort() fires only one close event"); diff --git a/tests/wpt/tests/close-watcher/basic.html b/tests/wpt/tests/close-watcher/basic.html index 9951e54031c..79a91e127ef 100644 --- a/tests/wpt/tests/close-watcher/basic.html +++ b/tests/wpt/tests/close-watcher/basic.html @@ -14,8 +14,8 @@ test(t => { watcher.requestClose(); - assert_array_equals(events, ["close"]); -}, "requestClose() with no user activation only fires close"); + assert_array_equals(events, ["cancel[cancelable=false]", "close"]); +}, "requestClose() with no user activation"); test(t => { let events = []; @@ -25,7 +25,7 @@ test(t => { watcher.requestClose(); assert_array_equals(events, []); -}, "destroy() then requestClose() fires no events"); +}, "destroy() then requestClose()"); test(t => { let events = []; @@ -36,18 +36,18 @@ test(t => { watcher.requestClose(); assert_array_equals(events, ["close"]); -}, "close() then requestClose() fires only one close event"); +}, "close() then requestClose()"); test(t => { let events = []; let watcher = createRecordingCloseWatcher(t, events); watcher.requestClose(); - assert_array_equals(events, ["close"]); + assert_array_equals(events, ["cancel[cancelable=false]", "close"]); watcher.destroy(); - assert_array_equals(events, ["close"]); -}, "requestClose() then destroy() fires only one close event"); + assert_array_equals(events, ["cancel[cancelable=false]", "close"]); +}, "requestClose() then destroy()"); test(t => { let events = []; @@ -58,7 +58,7 @@ test(t => { watcher.destroy(); assert_array_equals(events, ["close"]); -}, "close() then destroy() fires only one close event"); +}, "close() then destroy()"); promise_test(async t => { let events = []; @@ -68,7 +68,7 @@ promise_test(async t => { await sendCloseRequest(); assert_array_equals(events, []); -}, "destroy() then close request fires no events"); +}, "destroy() then close request"); promise_test(async t => { let events = []; @@ -77,6 +77,6 @@ promise_test(async t => { await sendCloseRequest(); watcher.destroy(); - assert_array_equals(events, ["close"]); -}, "Close request then destroy() fires only one close event"); + assert_array_equals(events, ["cancel[cancelable=false]", "close"]); +}, "Close request then destroy()"); </script> diff --git a/tests/wpt/tests/close-watcher/esc-key/keypress.html b/tests/wpt/tests/close-watcher/esc-key/keypress.html index 8dd58b064d7..c3bfcc0c330 100644 --- a/tests/wpt/tests/close-watcher/esc-key/keypress.html +++ b/tests/wpt/tests/close-watcher/esc-key/keypress.html @@ -16,6 +16,6 @@ promise_test(async t => { await sendEscKey(); - assert_array_equals(events, ["close"]); + assert_array_equals(events, ["cancel[cancelable=false]", "close"]); }, "A keypress listener can NOT prevent the Esc keypress from being interpreted as a close request"); </script> diff --git a/tests/wpt/tests/close-watcher/esc-key/keyup.html b/tests/wpt/tests/close-watcher/esc-key/keyup.html index 341012d6bc8..7c75ef7969c 100644 --- a/tests/wpt/tests/close-watcher/esc-key/keyup.html +++ b/tests/wpt/tests/close-watcher/esc-key/keyup.html @@ -16,6 +16,6 @@ promise_test(async t => { await sendEscKey(); - assert_array_equals(events, ["close"]); + assert_array_equals(events, ["cancel[cancelable=false]", "close"]); }, "A keyup listener can NOT prevent the Esc keypress from being interpreted as a close request"); </script> diff --git a/tests/wpt/tests/close-watcher/esc-key/not-user-activation.html b/tests/wpt/tests/close-watcher/esc-key/not-user-activation.html index ac29f84f06c..a8d5d22fcf8 100644 --- a/tests/wpt/tests/close-watcher/esc-key/not-user-activation.html +++ b/tests/wpt/tests/close-watcher/esc-key/not-user-activation.html @@ -14,6 +14,6 @@ promise_test(async t => { await sendEscKey(); - assert_array_equals(events, ["close"]); -}, "Esc key does not count as user activation, so if it is the sole user interaction, that fires close but not cancel"); + assert_array_equals(events, ["cancel[cancelable=false]", "close"]); +}, "Esc key does not count as user activation, so if it is the sole user interaction, cancel is cancelable=false"); </script> diff --git a/tests/wpt/tests/close-watcher/inside-event-listeners.html b/tests/wpt/tests/close-watcher/inside-event-listeners.html index ac037fc147f..47f431e2503 100644 --- a/tests/wpt/tests/close-watcher/inside-event-listeners.html +++ b/tests/wpt/tests/close-watcher/inside-event-listeners.html @@ -17,10 +17,10 @@ promise_test(async t => { await test_driver.bless("give user activation so that cancel will fire", () => { watcher.requestClose(); }); - assert_array_equals(events, ["cancel"]); + assert_array_equals(events, ["cancel[cancelable=true]"]); watcher.requestClose(); - assert_array_equals(events, ["cancel"], "since it was inactive, no more events fired"); + assert_array_equals(events, ["cancel[cancelable=true]"], "since it was inactive, no more events fired"); }, "destroy() inside oncancel"); test(t => { @@ -30,10 +30,10 @@ test(t => { watcher.onclose = () => { watcher.destroy(); } watcher.requestClose(); - assert_array_equals(events, ["close"]); + assert_array_equals(events, ["cancel[cancelable=false]", "close"]); watcher.requestClose(); - assert_array_equals(events, ["close"], "since it was inactive, no more events fired"); + assert_array_equals(events, ["cancel[cancelable=false]", "close"], "since it was inactive, no more events fired"); }, "destroy() inside onclose"); promise_test(async t => { @@ -45,10 +45,10 @@ promise_test(async t => { await test_driver.bless("give user activation so that cancel will fire", () => { watcher.requestClose(); }); - assert_array_equals(events, ["cancel", "close"]); + assert_array_equals(events, ["cancel[cancelable=true]", "close"]); watcher.requestClose(); - assert_array_equals(events, ["cancel", "close"], "since it was inactive, no more events fired"); + assert_array_equals(events, ["cancel[cancelable=true]", "close"], "since it was inactive, no more events fired"); }, "close() inside oncancel"); test(t => { @@ -58,10 +58,10 @@ test(t => { watcher.onclose = () => { watcher.close(); } watcher.requestClose(); - assert_array_equals(events, ["close"]); + assert_array_equals(events, ["cancel[cancelable=false]", "close"]); watcher.requestClose(); - assert_array_equals(events, ["close"], "since it was inactive, no more events fired"); + assert_array_equals(events, ["cancel[cancelable=false]", "close"], "since it was inactive, no more events fired"); }, "close() inside onclose"); promise_test(async t => { @@ -73,10 +73,10 @@ promise_test(async t => { await test_driver.bless("give user activation so that cancel will fire", () => { watcher.requestClose(); }); - assert_array_equals(events, ["cancel", "close"]); + assert_array_equals(events, ["cancel[cancelable=true]", "close"]); watcher.requestClose(); - assert_array_equals(events, ["cancel", "close"], "since it was inactive, no more events fired"); + assert_array_equals(events, ["cancel[cancelable=true]", "close"], "since it was inactive, no more events fired"); }, "requestClose() inside oncancel"); test(t => { @@ -86,9 +86,9 @@ test(t => { watcher.onclose = () => { watcher.requestClose(); } watcher.requestClose(); - assert_array_equals(events, ["close"]); + assert_array_equals(events, ["cancel[cancelable=false]", "close"]); watcher.requestClose(); - assert_array_equals(events, ["close"], "since it was inactive, no more events fired"); + assert_array_equals(events, ["cancel[cancelable=false]", "close"], "since it was inactive, no more events fired"); }, "requestClose() inside onclose"); </script> diff --git a/tests/wpt/tests/close-watcher/resources/helpers.js b/tests/wpt/tests/close-watcher/resources/helpers.js index dd9e191c4db..ad80c28847f 100644 --- a/tests/wpt/tests/close-watcher/resources/helpers.js +++ b/tests/wpt/tests/close-watcher/resources/helpers.js @@ -26,9 +26,14 @@ window.createRecordingCloseWatcher = (t, events, name, type, parentWatcher) => { t.add_cleanup(() => watcher.destroy()); } - const prefix = name === undefined ? "" : name + " "; - watcher.addEventListener('cancel', () => events.push(prefix + "cancel")); - watcher.addEventListener('close', () => events.push(prefix + "close")); + const prefix = name === undefined ? '' : name + ' '; + watcher.addEventListener('cancel', e => { + const cancelable = e.cancelable ? '[cancelable=true]' : '[cancelable=false]'; + events.push(prefix + 'cancel' + cancelable); + }); + watcher.addEventListener('close', () => { + events.push(prefix + 'close'); + }); return watcher; }; diff --git a/tests/wpt/tests/close-watcher/user-activation/README.md b/tests/wpt/tests/close-watcher/user-activation/README.md new file mode 100644 index 00000000000..b9aa9a21234 --- /dev/null +++ b/tests/wpt/tests/close-watcher/user-activation/README.md @@ -0,0 +1,25 @@ +# Close watcher user activation tests + +These tests are all in separate files (or test variants) because we need to be +sure we're starting from zero user activation. + +## Note on variants vs. `-dialog` and `-CloseWatcher` files + +We endeavor to have all the tests in these files cover both `<dialog>` elements +and the `CloseWatcher` API. (And sometimes the `popover=""` attribute.) + +When the test expectations are the same for both `<dialog>` and `CloseWatcher`, +we use WPT's variants feature. + +However, in some cases different expectations are necessary. This is because +`<dialog>`s queue a task to fire their `close` event, and do not queue a task +to fire their `cancel` event. Thus, when you have two `<dialog>`s grouped +together, you get the somewhat-strange behavior of both `cancel`s firing first, +then both `close`s. Whereas `CloseWatcher`s do not have this issue; both events +fire synchronously. + +(Note that scheduling the `cancel` event for `<dialog>`s is not really possible, +since it would then fire after the dialog has been closed in the DOM and +visually. So the only reasonable fix for this would be to stop scheduling the +`close` event for dialogs. That's risky from a compat standpoint, so for now, +we test the strange behavior.) diff --git a/tests/wpt/tests/close-watcher/user-activation/n-activate-preventDefault.html b/tests/wpt/tests/close-watcher/user-activation/n-activate-preventDefault.html index 531ef425998..f413448718e 100644 --- a/tests/wpt/tests/close-watcher/user-activation/n-activate-preventDefault.html +++ b/tests/wpt/tests/close-watcher/user-activation/n-activate-preventDefault.html @@ -22,10 +22,10 @@ promise_test(async t => { await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["cancel"]); + assert_array_equals(events, ["cancel[cancelable=true]"]); await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["cancel", "close"]); + assert_array_equals(events, ["cancel[cancelable=true]", "cancel[cancelable=false]", "close"]); }, "Create a close watcher without user activation that preventDefault()s cancel; send user activation"); </script> diff --git a/tests/wpt/tests/close-watcher/user-activation/n-activate.html b/tests/wpt/tests/close-watcher/user-activation/n-activate.html index babcf54c3c1..d8253ba7656 100644 --- a/tests/wpt/tests/close-watcher/user-activation/n-activate.html +++ b/tests/wpt/tests/close-watcher/user-activation/n-activate.html @@ -22,6 +22,6 @@ promise_test(async t => { await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["cancel", "close"]); + assert_array_equals(events, ["cancel[cancelable=true]", "close"]); }, "Create a close watcher without user activation; send user activation"); </script> diff --git a/tests/wpt/tests/close-watcher/user-activation/n-closerequest-n.html b/tests/wpt/tests/close-watcher/user-activation/n-closerequest-n.html index 2424af7820e..54ccdd1abe2 100644 --- a/tests/wpt/tests/close-watcher/user-activation/n-closerequest-n.html +++ b/tests/wpt/tests/close-watcher/user-activation/n-closerequest-n.html @@ -19,12 +19,12 @@ promise_test(async t => { await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher1 close"]); + assert_array_equals(events, ["watcher1 cancel[cancelable=false]", "watcher1 close"]); createRecordingCloseWatcher(t, events, "watcher2", type); await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher1 close", "watcher2 close"]); + assert_array_equals(events, ["watcher1 cancel[cancelable=false]", "watcher1 close", "watcher2 cancel[cancelable=false]", "watcher2 close"]); }, "Create a close watcher without user activation; send a close request; create a close watcher without user activation"); </script> diff --git a/tests/wpt/tests/close-watcher/user-activation/n-destroy-n.html b/tests/wpt/tests/close-watcher/user-activation/n-destroy-n.html index c26f87dd6f8..e0a94f490e8 100644 --- a/tests/wpt/tests/close-watcher/user-activation/n-destroy-n.html +++ b/tests/wpt/tests/close-watcher/user-activation/n-destroy-n.html @@ -26,6 +26,6 @@ promise_test(async t => { await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher2 close"]); + assert_array_equals(events, ["watcher2 cancel[cancelable=false]", "watcher2 close"]); }, "Create a close watcher without user activation; destroy the close watcher; create a close watcher without user activation"); </script> diff --git a/tests/wpt/tests/close-watcher/user-activation/n.html b/tests/wpt/tests/close-watcher/user-activation/n.html index fe04e0dc1b9..af8f972ee61 100644 --- a/tests/wpt/tests/close-watcher/user-activation/n.html +++ b/tests/wpt/tests/close-watcher/user-activation/n.html @@ -20,6 +20,6 @@ promise_test(async t => { await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["close"]); + assert_array_equals(events, ["cancel[cancelable=false]", "close"]); }, "Create a close watcher without user activation"); </script> diff --git a/tests/wpt/tests/close-watcher/user-activation/nn.html b/tests/wpt/tests/close-watcher/user-activation/nn-CloseWatcher.html index beb63f1b4f5..016745dfbb4 100644 --- a/tests/wpt/tests/close-watcher/user-activation/nn.html +++ b/tests/wpt/tests/close-watcher/user-activation/nn-CloseWatcher.html @@ -1,6 +1,4 @@ <!doctype html> -<meta name=variant content="?dialog"> -<meta name=variant content="?CloseWatcher"> <script src="/resources/testharness.js"></script> <script src="/resources/testharnessreport.js"></script> <script src="/resources/testdriver.js"></script> @@ -11,7 +9,7 @@ <body> <script> -const type = location.search.substring(1); +const type = "CloseWatcher"; promise_test(async t => { const events = []; @@ -21,6 +19,6 @@ promise_test(async t => { await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher2 close", "watcher1 close"]); + assert_array_equals(events, ["watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]); }, "Create two close watchers without user activation"); </script> diff --git a/tests/wpt/tests/close-watcher/user-activation/nn-activate-CloseWatcher.html b/tests/wpt/tests/close-watcher/user-activation/nn-activate-CloseWatcher.html index 8045f30b482..45718e51a88 100644 --- a/tests/wpt/tests/close-watcher/user-activation/nn-activate-CloseWatcher.html +++ b/tests/wpt/tests/close-watcher/user-activation/nn-activate-CloseWatcher.html @@ -7,10 +7,6 @@ <script src="/common/top-layer.js"></script> <script src="../resources/helpers.js"></script> -<!-- - See note in sibling -dialog.html file. ---> - <body> <script> const type = "CloseWatcher"; @@ -25,6 +21,6 @@ promise_test(async t => { await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher2 cancel", "watcher2 close", "watcher1 cancel", "watcher1 close"]); + assert_array_equals(events, ["watcher2 cancel[cancelable=true]", "watcher2 close", "watcher1 cancel[cancelable=true]", "watcher1 close"]); }, "Create two CloseWatchers without user activation; send user activation"); </script> diff --git a/tests/wpt/tests/close-watcher/user-activation/nn-activate-dialog.html b/tests/wpt/tests/close-watcher/user-activation/nn-activate-dialog.html index 5cc866044ce..eaffb4d9a7b 100644 --- a/tests/wpt/tests/close-watcher/user-activation/nn-activate-dialog.html +++ b/tests/wpt/tests/close-watcher/user-activation/nn-activate-dialog.html @@ -7,20 +7,6 @@ <script src="/common/top-layer.js"></script> <script src="../resources/helpers.js"></script> -<!-- - This test has different expectations for dialogs vs. CloseWatchers because - dialogs queue a task to fire their close event, and do not do so for their - cancel event. Thus, when you have two dialogs grouped together, you get the - somewhat-strange behavior of both cancels firing first, then both closes. - Whereas CloseWatchers do not have this issue; both fire synchronously. - - Note that scheduling the cancel event for dialogs is not really possible since - it would then fire after the dialog has been closed in the DOM and visually. - So the only reasonable fix for this would be to stop scheduling the close - event for dialogs. That's risky from a compat standpoint, so for now, test the - strange behavior. ---> - <body> <script> const type = "dialog"; @@ -35,6 +21,6 @@ promise_test(async t => { await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher2 cancel", "watcher1 cancel", "watcher2 close", "watcher1 close"]); + assert_array_equals(events, ["watcher2 cancel[cancelable=true]", "watcher1 cancel[cancelable=true]", "watcher2 close", "watcher1 close"]); }, "Create two dialogs without user activation; send user activation"); </script> diff --git a/tests/wpt/tests/close-watcher/user-activation/nn-dialog.html b/tests/wpt/tests/close-watcher/user-activation/nn-dialog.html new file mode 100644 index 00000000000..0d086a525ef --- /dev/null +++ b/tests/wpt/tests/close-watcher/user-activation/nn-dialog.html @@ -0,0 +1,24 @@ +<!doctype html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="/common/top-layer.js"></script> +<script src="../resources/helpers.js"></script> + +<body> +<script> +const type = "dialog"; + +promise_test(async t => { + const events = []; + + createRecordingCloseWatcher(t, events, "watcher1", type); + createRecordingCloseWatcher(t, events, "watcher2", type); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["watcher2 cancel[cancelable=false]", "watcher1 cancel[cancelable=false]", "watcher2 close", "watcher1 close"]); +}, "Create two close watchers without user activation"); +</script> diff --git a/tests/wpt/tests/close-watcher/user-activation/nnn-CloseWatcher-dialog-popover.html b/tests/wpt/tests/close-watcher/user-activation/nnn-CloseWatcher-dialog-popover.html index f8b9061d019..38dd6073121 100644 --- a/tests/wpt/tests/close-watcher/user-activation/nnn-CloseWatcher-dialog-popover.html +++ b/tests/wpt/tests/close-watcher/user-activation/nnn-CloseWatcher-dialog-popover.html @@ -30,6 +30,6 @@ promise_test(async t => { assert_false(popover.matches(':popover-open'), 'The popover should be closed.'); assert_false(dialog.hasAttribute('open'), 'The dialog should be closed.'); - assert_array_equals(events, ['CloseWatcher close', 'dialog close']); + assert_array_equals(events, ['dialog cancel[cancelable=false]', 'CloseWatcher cancel[cancelable=false]', 'CloseWatcher close', 'dialog close']); }, 'Create a CloseWatcher without user activation; create a dialog without user activation; create a popover without user activation'); </script> diff --git a/tests/wpt/tests/close-watcher/user-activation/nnn-CloseWatcher.html b/tests/wpt/tests/close-watcher/user-activation/nnn-CloseWatcher.html new file mode 100644 index 00000000000..5d2f07e6173 --- /dev/null +++ b/tests/wpt/tests/close-watcher/user-activation/nnn-CloseWatcher.html @@ -0,0 +1,25 @@ +<!doctype html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="/common/top-layer.js"></script> +<script src="../resources/helpers.js"></script> + +<body> +<script> +const type = "CloseWatcher"; + +promise_test(async t => { + const events = []; + + createRecordingCloseWatcher(t, events, "watcher1", type); + createRecordingCloseWatcher(t, events, "watcher2", type); + createRecordingCloseWatcher(t, events, "watcher3", type); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]); +}, "Create three close watchers without user activation"); +</script> diff --git a/tests/wpt/tests/close-watcher/user-activation/nnn.html b/tests/wpt/tests/close-watcher/user-activation/nnn-dialog.html index 9b604e91db2..f1c071dbb3e 100644 --- a/tests/wpt/tests/close-watcher/user-activation/nnn.html +++ b/tests/wpt/tests/close-watcher/user-activation/nnn-dialog.html @@ -1,6 +1,4 @@ <!doctype html> -<meta name=variant content="?dialog"> -<meta name=variant content="?CloseWatcher"> <script src="/resources/testharness.js"></script> <script src="/resources/testharnessreport.js"></script> <script src="/resources/testdriver.js"></script> @@ -11,7 +9,7 @@ <body> <script> -const type = location.search.substring(1); +const type = "dialog"; promise_test(async t => { const events = []; @@ -22,6 +20,6 @@ promise_test(async t => { await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher3 close", "watcher2 close", "watcher1 close"]); + assert_array_equals(events, ["watcher3 cancel[cancelable=false]", "watcher2 cancel[cancelable=false]", "watcher1 cancel[cancelable=false]", "watcher3 close", "watcher2 close", "watcher1 close"]); }, "Create three close watchers without user activation"); </script> diff --git a/tests/wpt/tests/close-watcher/user-activation/ny-activate-preventDefault.html b/tests/wpt/tests/close-watcher/user-activation/ny-activate-preventDefault.html index 5ffb64b1134..7cd1c2e5080 100644 --- a/tests/wpt/tests/close-watcher/user-activation/ny-activate-preventDefault.html +++ b/tests/wpt/tests/close-watcher/user-activation/ny-activate-preventDefault.html @@ -24,14 +24,14 @@ promise_test(async t => { await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher2 cancel"]); + assert_array_equals(events, ["watcher2 cancel[cancelable=true]"]); await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher2 cancel", "watcher2 close"]); + assert_array_equals(events, ["watcher2 cancel[cancelable=true]", "watcher2 cancel[cancelable=false]", "watcher2 close"]); await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher2 cancel", "watcher2 close", "watcher1 close"]); + assert_array_equals(events, ["watcher2 cancel[cancelable=true]", "watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]); }, "Create a close watcher without user activation; create a close watcher with user activation that preventDefault()s cancel; send user activation"); </script> diff --git a/tests/wpt/tests/close-watcher/user-activation/ny.html b/tests/wpt/tests/close-watcher/user-activation/ny.html index 226912233e2..49f50a123e9 100644 --- a/tests/wpt/tests/close-watcher/user-activation/ny.html +++ b/tests/wpt/tests/close-watcher/user-activation/ny.html @@ -21,10 +21,10 @@ promise_test(async t => { await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher2 close"]); + assert_array_equals(events, ["watcher2 cancel[cancelable=false]", "watcher2 close"]); await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher2 close", "watcher1 close"]); + assert_array_equals(events, ["watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]); }, "Create a close watcher without user activation; create a close watcher with user activation"); </script> diff --git a/tests/wpt/tests/close-watcher/user-activation/nyn.html b/tests/wpt/tests/close-watcher/user-activation/nyn.html index ec5153c7674..b227d566d4c 100644 --- a/tests/wpt/tests/close-watcher/user-activation/nyn.html +++ b/tests/wpt/tests/close-watcher/user-activation/nyn.html @@ -21,10 +21,10 @@ promise_test(async t => { await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher3 close", "watcher2 close"]); + assert_array_equals(events, ["watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close"]); await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher3 close", "watcher2 close", "watcher1 close"]); + assert_array_equals(events, ["watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]); }, "Create a close watcher without user activation; create a close watcher with user activation; create a close watcher without user activation"); </script> diff --git a/tests/wpt/tests/close-watcher/user-activation/nynn-destroy.html b/tests/wpt/tests/close-watcher/user-activation/nynn-destroy.html index 8519c8a2a94..fb04109994d 100644 --- a/tests/wpt/tests/close-watcher/user-activation/nynn-destroy.html +++ b/tests/wpt/tests/close-watcher/user-activation/nynn-destroy.html @@ -24,10 +24,10 @@ promise_test(async t => { await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher4 close", "watcher3 close"]); + assert_array_equals(events, ["watcher4 cancel[cancelable=false]", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close"]); await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher4 close", "watcher3 close", "watcher1 close"]); + assert_array_equals(events, ["watcher4 cancel[cancelable=false]", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]); }, "Create a close watcher without user activation; create a close watcher with user activation; create two close watchers without user activation; remove the second close watcher"); </script> diff --git a/tests/wpt/tests/close-watcher/user-activation/nynn.html b/tests/wpt/tests/close-watcher/user-activation/nynn.html index f6e74a0ba11..ed9203db665 100644 --- a/tests/wpt/tests/close-watcher/user-activation/nynn.html +++ b/tests/wpt/tests/close-watcher/user-activation/nynn.html @@ -22,10 +22,10 @@ promise_test(async t => { await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher4 close", "watcher3 close", "watcher2 close"]); + assert_array_equals(events, ["watcher4 cancel[cancelable=false]", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close"]); await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher4 close", "watcher3 close", "watcher2 close", "watcher1 close"]); + assert_array_equals(events, ["watcher4 cancel[cancelable=false]", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]); }, "Create a close watcher without user activation; create a close watcher with user activation; create two close watchers without user activation"); </script> diff --git a/tests/wpt/tests/close-watcher/user-activation/nyyn-CloseWatcher.html b/tests/wpt/tests/close-watcher/user-activation/nyyn-CloseWatcher.html new file mode 100644 index 00000000000..4f60ef3c4ba --- /dev/null +++ b/tests/wpt/tests/close-watcher/user-activation/nyyn-CloseWatcher.html @@ -0,0 +1,34 @@ +<!doctype html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="/common/top-layer.js"></script> +<script src="../resources/helpers.js"></script> + +<body> +<script> +const type = "CloseWatcher"; + +promise_test(async t => { + const events = []; + + const watcher1 = createRecordingCloseWatcher(t, events, "watcher1", type); + const watcher2 = await createBlessedRecordingCloseWatcher(t, events, "watcher2", type, watcher1); + const watcher3 = await createBlessedRecordingCloseWatcher(t, events, "watcher3", type, watcher2); + createRecordingCloseWatcher(t, events, "watcher4", type); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["watcher4 cancel[cancelable=false]", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close"]); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["watcher4 cancel[cancelable=false]", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close"]); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["watcher4 cancel[cancelable=false]", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]); +}, "Create a close watcher without user activation; create two close watchers with user activation; create a close watcher without user activation"); +</script> diff --git a/tests/wpt/tests/close-watcher/user-activation/nyyn.html b/tests/wpt/tests/close-watcher/user-activation/nyyn-dialog.html index f3987c1a214..44926fd5c3e 100644 --- a/tests/wpt/tests/close-watcher/user-activation/nyyn.html +++ b/tests/wpt/tests/close-watcher/user-activation/nyyn-dialog.html @@ -1,6 +1,4 @@ <!doctype html> -<meta name=variant content="?dialog"> -<meta name=variant content="?CloseWatcher"> <script src="/resources/testharness.js"></script> <script src="/resources/testharnessreport.js"></script> <script src="/resources/testdriver.js"></script> @@ -11,7 +9,7 @@ <body> <script> -const type = location.search.substring(1); +const type = "dialog"; promise_test(async t => { const events = []; @@ -23,14 +21,14 @@ promise_test(async t => { await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher4 close", "watcher3 close"]); + assert_array_equals(events, ["watcher4 cancel[cancelable=false]", "watcher3 cancel[cancelable=false]", "watcher4 close", "watcher3 close"]); await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher4 close", "watcher3 close", "watcher2 close"]); + assert_array_equals(events, ["watcher4 cancel[cancelable=false]", "watcher3 cancel[cancelable=false]", "watcher4 close", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close"]); await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher4 close", "watcher3 close", "watcher2 close", "watcher1 close"]); + assert_array_equals(events, ["watcher4 cancel[cancelable=false]", "watcher3 cancel[cancelable=false]", "watcher4 close", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]); }, "Create a close watcher without user activation; create two close watchers with user activation; create a close watcher without user activation"); </script> diff --git a/tests/wpt/tests/close-watcher/user-activation/nyyyn-CloseWatcher.html b/tests/wpt/tests/close-watcher/user-activation/nyyyn-CloseWatcher.html new file mode 100644 index 00000000000..e2565a82a30 --- /dev/null +++ b/tests/wpt/tests/close-watcher/user-activation/nyyyn-CloseWatcher.html @@ -0,0 +1,38 @@ +<!doctype html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="/common/top-layer.js"></script> +<script src="../resources/helpers.js"></script> + +<body> +<script> +const type = "CloseWatcher"; + +promise_test(async t => { + const events = []; + const watcher1 = createRecordingCloseWatcher(t, events, "watcher1", type); + const watcher2 = await createBlessedRecordingCloseWatcher(t, events, "watcher2", type, watcher1); + const watcher3 = await createBlessedRecordingCloseWatcher(t, events, "watcher3", type, watcher2); + await createBlessedRecordingCloseWatcher(t, events, "watcher4", type, watcher3); + createRecordingCloseWatcher(t, events, "watcher5", type); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["watcher5 cancel[cancelable=false]", "watcher5 close", "watcher4 cancel[cancelable=false]", "watcher4 close"]); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["watcher5 cancel[cancelable=false]", "watcher5 close", "watcher4 cancel[cancelable=false]", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close"]); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["watcher5 cancel[cancelable=false]", "watcher5 close", "watcher4 cancel[cancelable=false]", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close"]); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["watcher5 cancel[cancelable=false]", "watcher5 close", "watcher4 cancel[cancelable=false]", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]); +}, "Create a close watcher without user activation; create three close watchers with user activation; create a close watcher without user activation"); +</script> diff --git a/tests/wpt/tests/close-watcher/user-activation/nyyyn.html b/tests/wpt/tests/close-watcher/user-activation/nyyyn-dialog.html index 6cb8f3a4456..86361124d30 100644 --- a/tests/wpt/tests/close-watcher/user-activation/nyyyn.html +++ b/tests/wpt/tests/close-watcher/user-activation/nyyyn-dialog.html @@ -1,6 +1,4 @@ <!doctype html> -<meta name=variant content="?dialog"> -<meta name=variant content="?CloseWatcher"> <script src="/resources/testharness.js"></script> <script src="/resources/testharnessreport.js"></script> <script src="/resources/testdriver.js"></script> @@ -11,7 +9,7 @@ <body> <script> -const type = location.search.substring(1); +const type = "dialog"; promise_test(async t => { const events = []; @@ -23,18 +21,18 @@ promise_test(async t => { await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher5 close", "watcher4 close"]); + assert_array_equals(events, ["watcher5 cancel[cancelable=false]", "watcher4 cancel[cancelable=false]", "watcher5 close", "watcher4 close"]); await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher5 close", "watcher4 close", "watcher3 close"]); + assert_array_equals(events, ["watcher5 cancel[cancelable=false]", "watcher4 cancel[cancelable=false]", "watcher5 close", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close"]); await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher5 close", "watcher4 close", "watcher3 close", "watcher2 close"]); + assert_array_equals(events, ["watcher5 cancel[cancelable=false]", "watcher4 cancel[cancelable=false]", "watcher5 close", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close"]); await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher5 close", "watcher4 close", "watcher3 close", "watcher2 close", "watcher1 close"]); + assert_array_equals(events, ["watcher5 cancel[cancelable=false]", "watcher4 cancel[cancelable=false]", "watcher5 close", "watcher4 close", "watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]); }, "Create a close watcher without user activation; create three close watchers with user activation; create a close watcher without user activation"); </script> diff --git a/tests/wpt/tests/close-watcher/user-activation/y.html b/tests/wpt/tests/close-watcher/user-activation/y.html index ee58a92293a..78c432de388 100644 --- a/tests/wpt/tests/close-watcher/user-activation/y.html +++ b/tests/wpt/tests/close-watcher/user-activation/y.html @@ -20,6 +20,6 @@ promise_test(async t => { await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["cancel", "close"]); + assert_array_equals(events, ["cancel[cancelable=true]", "close"]); }, "Create a close watcher with user activation"); </script> diff --git a/tests/wpt/tests/close-watcher/user-activation/yn-activate.html b/tests/wpt/tests/close-watcher/user-activation/yn-activate.html index af7289aa28e..d62b4df4250 100644 --- a/tests/wpt/tests/close-watcher/user-activation/yn-activate.html +++ b/tests/wpt/tests/close-watcher/user-activation/yn-activate.html @@ -23,10 +23,10 @@ promise_test(async t => { await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher2 cancel", "watcher2 close"]); + assert_array_equals(events, ["watcher2 cancel[cancelable=true]", "watcher2 close"]); await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher2 cancel", "watcher2 close", "watcher1 cancel", "watcher1 close"]); + assert_array_equals(events, ["watcher2 cancel[cancelable=true]", "watcher2 close", "watcher1 cancel[cancelable=true]", "watcher1 close"]); }, "Create a close watcher with user activation; create a close watcher without user activation; send user activation"); </script> diff --git a/tests/wpt/tests/close-watcher/user-activation/yn.html b/tests/wpt/tests/close-watcher/user-activation/yn.html index 8f7e90e2d85..578f43de25e 100644 --- a/tests/wpt/tests/close-watcher/user-activation/yn.html +++ b/tests/wpt/tests/close-watcher/user-activation/yn.html @@ -21,10 +21,10 @@ promise_test(async t => { await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher2 close"]); + assert_array_equals(events, ["watcher2 cancel[cancelable=false]", "watcher2 close"]); await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher2 close", "watcher1 close"]); + assert_array_equals(events, ["watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]); }, "Create a close watcher with user activation; create a close watcher without user activation"); </script> diff --git a/tests/wpt/tests/close-watcher/user-activation/ynn-CloseWatcher.html b/tests/wpt/tests/close-watcher/user-activation/ynn-CloseWatcher.html new file mode 100644 index 00000000000..50b5a8131d1 --- /dev/null +++ b/tests/wpt/tests/close-watcher/user-activation/ynn-CloseWatcher.html @@ -0,0 +1,29 @@ +<!doctype html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="/common/top-layer.js"></script> +<script src="../resources/helpers.js"></script> + +<body> +<script> +const type = "CloseWatcher"; + +promise_test(async t => { + const events = []; + + await createBlessedRecordingCloseWatcher(t, events, "watcher1", type); + createRecordingCloseWatcher(t, events, "watcher2", type); + createRecordingCloseWatcher(t, events, "watcher3", type); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close"]); + + await sendCloseRequest(); + await waitForPotentialCloseEvent(); + assert_array_equals(events, ["watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]); +}, "Create a close watcher with user activation; create two close watchers without user activation"); +</script> diff --git a/tests/wpt/tests/close-watcher/user-activation/ynn.html b/tests/wpt/tests/close-watcher/user-activation/ynn-dialog.html index 8cc7f5bfb66..c10e94dc735 100644 --- a/tests/wpt/tests/close-watcher/user-activation/ynn.html +++ b/tests/wpt/tests/close-watcher/user-activation/ynn-dialog.html @@ -1,6 +1,4 @@ <!doctype html> -<meta name=variant content="?dialog"> -<meta name=variant content="?CloseWatcher"> <script src="/resources/testharness.js"></script> <script src="/resources/testharnessreport.js"></script> <script src="/resources/testdriver.js"></script> @@ -11,7 +9,7 @@ <body> <script> -const type = location.search.substring(1); +const type = "dialog"; promise_test(async t => { const events = []; @@ -22,10 +20,10 @@ promise_test(async t => { await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher3 close", "watcher2 close"]); + assert_array_equals(events, ["watcher3 cancel[cancelable=false]", "watcher2 cancel[cancelable=false]", "watcher3 close", "watcher2 close"]); await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher3 close", "watcher2 close", "watcher1 close"]); + assert_array_equals(events, ["watcher3 cancel[cancelable=false]", "watcher2 cancel[cancelable=false]", "watcher3 close", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]); }, "Create a close watcher with user activation; create two close watchers without user activation"); </script> diff --git a/tests/wpt/tests/close-watcher/user-activation/yy.html b/tests/wpt/tests/close-watcher/user-activation/yy.html index 0aa03cdd050..9c0f21be22f 100644 --- a/tests/wpt/tests/close-watcher/user-activation/yy.html +++ b/tests/wpt/tests/close-watcher/user-activation/yy.html @@ -21,10 +21,10 @@ promise_test(async t => { await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher2 cancel", "watcher2 close"]); + assert_array_equals(events, ["watcher2 cancel[cancelable=true]", "watcher2 close"]); await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher2 cancel", "watcher2 close", "watcher1 cancel", "watcher1 close"]); + assert_array_equals(events, ["watcher2 cancel[cancelable=true]", "watcher2 close", "watcher1 cancel[cancelable=true]", "watcher1 close"]); }, "Create two close watchers with user activation"); </script> diff --git a/tests/wpt/tests/close-watcher/user-activation/yyn.html b/tests/wpt/tests/close-watcher/user-activation/yyn.html index b87cf7a7e34..2f753774446 100644 --- a/tests/wpt/tests/close-watcher/user-activation/yyn.html +++ b/tests/wpt/tests/close-watcher/user-activation/yyn.html @@ -22,14 +22,14 @@ promise_test(async t => { await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher3 close"]); + assert_array_equals(events, ["watcher3 cancel[cancelable=false]", "watcher3 close"]); await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher3 close", "watcher2 close"]); + assert_array_equals(events, ["watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close"]); await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher3 close", "watcher2 close", "watcher1 close"]); + assert_array_equals(events, ["watcher3 cancel[cancelable=false]", "watcher3 close", "watcher2 cancel[cancelable=false]", "watcher2 close", "watcher1 cancel[cancelable=false]", "watcher1 close"]); }, "Create two close watchers with user activation; create a close watcher without user activation"); </script> diff --git a/tests/wpt/tests/close-watcher/user-activation/yyy-CloseWatcher-dialog-popover.html b/tests/wpt/tests/close-watcher/user-activation/yyy-CloseWatcher-dialog-popover.html index f0a1cb06d10..8650fb3b7c4 100644 --- a/tests/wpt/tests/close-watcher/user-activation/yyy-CloseWatcher-dialog-popover.html +++ b/tests/wpt/tests/close-watcher/user-activation/yyy-CloseWatcher-dialog-popover.html @@ -35,12 +35,12 @@ promise_test(async t => { await waitForPotentialCloseEvent(); assert_false(popover.matches(':popover-open'), 'Second close request: The popover should be closed.'); assert_false(dialog.hasAttribute('open'), 'Second close request: The dialog should be closed.'); - assert_array_equals(events, ['dialog cancel', 'dialog close']); + assert_array_equals(events, ['dialog cancel[cancelable=true]', 'dialog close']); await sendCloseRequest(); await waitForPotentialCloseEvent(); assert_false(popover.matches(':popover-open'), 'Third close request: The popover should be closed.'); assert_false(dialog.hasAttribute('open'), 'Third close request: The dialog should be closed.'); - assert_array_equals(events, ['dialog cancel', 'dialog close', 'CloseWatcher cancel', 'CloseWatcher close']); + assert_array_equals(events, ['dialog cancel[cancelable=true]', 'dialog close', 'CloseWatcher cancel[cancelable=true]', 'CloseWatcher close']); }, 'Create a CloseWatcher with user activation; create a dialog with user activation; create a popover with user activation'); </script> diff --git a/tests/wpt/tests/close-watcher/user-activation/yyy-activate-CloseWatcher-dialog-popover.html b/tests/wpt/tests/close-watcher/user-activation/yyy-activate-CloseWatcher-dialog-popover.html index ed41d1bc321..a58dd0751bb 100644 --- a/tests/wpt/tests/close-watcher/user-activation/yyy-activate-CloseWatcher-dialog-popover.html +++ b/tests/wpt/tests/close-watcher/user-activation/yyy-activate-CloseWatcher-dialog-popover.html @@ -37,13 +37,13 @@ promise_test(async t => { await waitForPotentialCloseEvent(); assert_false(popover.matches(':popover-open'), 'Second close request: The popover should be closed.'); assert_false(dialog.hasAttribute('open'), 'Second close request: The dialog should be closed.'); - assert_array_equals(events, ['dialog cancel', 'dialog close']); + assert_array_equals(events, ['dialog cancel[cancelable=true]', 'dialog close']); await test_driver.bless(); await sendCloseRequest(); await waitForPotentialCloseEvent(); assert_false(popover.matches(':popover-open'), 'Third close request: The popover should be closed.'); assert_false(dialog.hasAttribute('open'), 'Third close request: The dialog should be closed.'); - assert_array_equals(events, ['dialog cancel', 'dialog close', 'CloseWatcher cancel', 'CloseWatcher close']); + assert_array_equals(events, ['dialog cancel[cancelable=true]', 'dialog close', 'CloseWatcher cancel[cancelable=true]', 'CloseWatcher close']); }, 'Create a CloseWatcher with user activation; create a dialog with user activation; create a popover with user activation; sending user activation before each close request'); </script> diff --git a/tests/wpt/tests/close-watcher/user-activation/yyy.html b/tests/wpt/tests/close-watcher/user-activation/yyy.html index f16767a86bd..eaf8944bcec 100644 --- a/tests/wpt/tests/close-watcher/user-activation/yyy.html +++ b/tests/wpt/tests/close-watcher/user-activation/yyy.html @@ -22,14 +22,14 @@ promise_test(async t => { await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher3 cancel", "watcher3 close"]); + assert_array_equals(events, ["watcher3 cancel[cancelable=true]", "watcher3 close"]); await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher3 cancel", "watcher3 close", "watcher2 cancel", "watcher2 close"]); + assert_array_equals(events, ["watcher3 cancel[cancelable=true]", "watcher3 close", "watcher2 cancel[cancelable=true]", "watcher2 close"]); await sendCloseRequest(); await waitForPotentialCloseEvent(); - assert_array_equals(events, ["watcher3 cancel", "watcher3 close", "watcher2 cancel", "watcher2 close", "watcher1 cancel", "watcher1 close"]); + assert_array_equals(events, ["watcher3 cancel[cancelable=true]", "watcher3 close", "watcher2 cancel[cancelable=true]", "watcher2 close", "watcher1 cancel[cancelable=true]", "watcher1 close"]); }, "Create three close watchers with user activation"); </script> diff --git a/tests/wpt/tests/content-security-policy/generic/wildcard-host-part.sub.window.js b/tests/wpt/tests/content-security-policy/generic/wildcard-host-part.sub.window.js new file mode 100644 index 00000000000..d210cc6670f --- /dev/null +++ b/tests/wpt/tests/content-security-policy/generic/wildcard-host-part.sub.window.js @@ -0,0 +1,27 @@ +setup(_ => { + const meta = document.createElement("meta"); + meta.httpEquiv = "content-security-policy"; + meta.content = "img-src http://*:{{ports[http][0]}}"; + document.head.appendChild(meta); +}); + +async_test((t) => { + const img = document.createElement("img"); + img.onerror = t.step_func_done(); + img.onload = t.unreached_func("`data:` image should have been blocked."); + img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" +}, "Host wildcard doesn't affect scheme matching."); + +async_test((t) => { + const img = document.createElement("img"); + img.onload = t.step_func_done(); + img.onerror = t.unreached_func("Image from www2 host should have loaded."); + img.src = "http://{{domains[www1]}}:{{ports[http][0]}}/content-security-policy/support/pass.png"; +}, "Host wildcard allows arbitrary hosts (www1)."); + +async_test((t) => { + const img = document.createElement("img"); + img.onload = t.step_func_done(); + img.onerror = t.unreached_func("Image from www2 host should have loaded."); + img.src = "http://{{domains[www2]}}:{{ports[http][0]}}/content-security-policy/support/pass.png"; +}, "Host wildcard allows arbitrary hosts (www2)."); diff --git a/tests/wpt/tests/credential-management/fedcm-context.https.html b/tests/wpt/tests/credential-management/fedcm-context.https.html index 7b3e1032af9..f235437b789 100644 --- a/tests/wpt/tests/credential-management/fedcm-context.https.html +++ b/tests/wpt/tests/credential-management/fedcm-context.https.html @@ -12,37 +12,38 @@ import {request_options_with_mediation_required, request_options_with_context, fedcm_get_title_promise, - fedcm_test} from './support/fedcm-helper.sub.js'; + fedcm_test, + fedcm_select_account_promise} from './support/fedcm-helper.sub.js'; fedcm_test(async t => { - let p = navigator.credentials.get(request_options_with_mediation_required()); + const p = navigator.credentials.get(request_options_with_mediation_required()); const result = await fedcm_get_title_promise(t); assert_true(result.title.toLowerCase().includes('sign in')); - window.test_driver.select_fedcm_account(0); + fedcm_select_account_promise(t, 0); return p; }, "FedCM call defaults to 'signin' context."); fedcm_test(async t => { - let p = navigator.credentials.get(request_options_with_context("manifest.py", "signup")); + const p = navigator.credentials.get(request_options_with_context("manifest.py", "signup")); const result = await fedcm_get_title_promise(t); assert_true(result.title.toLowerCase().includes('sign up')); - window.test_driver.select_fedcm_account(0); + fedcm_select_account_promise(t, 0); return p; }, "FedCM with 'signup' context."); fedcm_test(async t => { - let p = navigator.credentials.get(request_options_with_context("manifest.py", "use")); + const p = navigator.credentials.get(request_options_with_context("manifest.py", "use")); const result = await fedcm_get_title_promise(t); assert_true(result.title.toLowerCase().includes('use')); - window.test_driver.select_fedcm_account(0); + fedcm_select_account_promise(t, 0); return p; }, "FedCM with 'use' context."); fedcm_test(async t => { - let p = navigator.credentials.get(request_options_with_context("manifest.py", "continue")); + const p = navigator.credentials.get(request_options_with_context("manifest.py", "continue")); const result = await fedcm_get_title_promise(t); assert_true(result.title.toLowerCase().includes('continue')); - window.test_driver.select_fedcm_account(0); + fedcm_select_account_promise(t, 0); return p; }, "FedCM with 'continue' context."); </script> diff --git a/tests/wpt/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-abort.https.html b/tests/wpt/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-abort.https.html new file mode 100644 index 00000000000..712a7b6a349 --- /dev/null +++ b/tests/wpt/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-abort.https.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<title>Federated Credential Management API multi IDP abort.</title> +<link rel="help" href="https://fedidcg.github.io/FedCM"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> + +<script type="module"> +import {request_options_with_two_idps, + fedcm_test, + fedcm_get_and_select_first_account} from '../support/fedcm-helper.sub.js'; + +fedcm_test(async t => { + let controller = new AbortController(); + let test_options = request_options_with_two_idps(); + test_options.signal = controller.signal; + const cred = fedcm_get_and_select_first_account(t, test_options); + controller.abort(); + return promise_rejects_dom(t, 'AbortError', cred); +}, "Test that the abort signal works when multiple IDPs are used."); +</script> diff --git a/tests/wpt/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-basic.https.html b/tests/wpt/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-basic.https.html new file mode 100644 index 00000000000..d855e0ad8dc --- /dev/null +++ b/tests/wpt/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-basic.https.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<title>Federated Credential Management API multi IDP basic success tests.</title> +<link rel="help" href="https://fedidcg.github.io/FedCM"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> + +<body> + +<script type="module"> +import {fedcm_test, + fedcm_get_and_select_first_account, + request_options_with_two_idps, + manifest_origin, + default_manifest_path, + fedcm_select_account_promise, + alt_manifest_origin} from '../support/fedcm-helper.sub.js'; + +fedcm_test(async t => { + const cred = await fedcm_get_and_select_first_account(t, request_options_with_two_idps()); + assert_equals(cred.token, "token"); + assert_equals(cred.configURL, manifest_origin + default_manifest_path); +}, "Multi IDP FedCM call succeeds when picking the first account."); + +fedcm_test(async t => { + const promise = navigator.credentials.get(request_options_with_two_idps()); + // Each IDP has one account, so select the second one. + fedcm_select_account_promise(t, 1); + const cred = await promise; + assert_equals(cred.token, "token"); + assert_equals(cred.configURL, alt_manifest_origin + default_manifest_path); +}, "Multi IDP FedCM call succeeds when picking account from the second IDP."); +</script> diff --git a/tests/wpt/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-context.https.html b/tests/wpt/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-context.https.html new file mode 100644 index 00000000000..1bc3eb1f562 --- /dev/null +++ b/tests/wpt/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-context.https.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<title>Federated Credential Management API multi IDP context tests.</title> +<link rel="help" href="https://fedidcg.github.io/FedCM"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> + +<body> + +<script type="module"> +import {request_options_with_two_idps, + fedcm_get_title_promise, + fedcm_test, + fedcm_select_account_promise} from '../support/fedcm-helper.sub.js'; + +fedcm_test(async t => { + const p = navigator.credentials.get(request_options_with_two_idps()); + const result = await fedcm_get_title_promise(t); + assert_true(result.title.toLowerCase().includes('sign in')); + fedcm_select_account_promise(t, 0); + return p; +}, "FedCM multi IDP call defaults to 'signin' context."); + +fedcm_test(async t => { + const options = request_options_with_two_idps(); + options.identity.context = "signup"; + const p = navigator.credentials.get(options); + const result = await fedcm_get_title_promise(t); + assert_true(result.title.toLowerCase().includes('sign up')); + fedcm_select_account_promise(t, 0); + return p; +}, "FedCM multi IDP with non-default context."); +</script> diff --git a/tests/wpt/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-mediation-optional.https.html b/tests/wpt/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-mediation-optional.https.html new file mode 100644 index 00000000000..1a819efb314 --- /dev/null +++ b/tests/wpt/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-mediation-optional.https.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<title>Federated Credential Management API multi IDP optional mediation tests.</title> +<link rel="help" href="https://fedidcg.github.io/FedCM"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> + +<script type="module"> +import {request_options_with_two_idps, + fedcm_test, + fedcm_get_and_select_first_account, + fedcm_select_account_promise} from '../support/fedcm-helper.sub.js'; + +fedcm_test(async t => { + return fedcm_get_and_select_first_account(t, request_options_with_two_idps('optional')); +}, "Mediation optional can succeed without a returning account."); + +fedcm_test(async t => { + // Sign in to the first account. + await fedcm_get_and_select_first_account(t, request_options_with_two_idps()); + + // Now use mediation:optional and it should work. + return navigator.credentials.get(request_options_with_two_idps('optional')); +}, "Mediation optional automatically succeeds when there is one returning account."); + +fedcm_test(async t => { + // Sign in to the first account. + await fedcm_get_and_select_first_account(t, request_options_with_two_idps()); + + // Sign in to the second account as well. + let cred = navigator.credentials.get(request_options_with_two_idps()); + fedcm_select_account_promise(t, 1); + await cred; + + // Now use mediation:optional. + return fedcm_get_and_select_first_account(t, request_options_with_two_idps('optional')); +}, "Mediation optional can succeed when there is more than one returning account."); +</script> diff --git a/tests/wpt/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-mediation-silent.https.html b/tests/wpt/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-mediation-silent.https.html new file mode 100644 index 00000000000..d47d4898c7d --- /dev/null +++ b/tests/wpt/tests/credential-management/fedcm-multi-idp/fedcm-multi-idp-mediation-silent.https.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<title>Federated Credential Management API multi IDP silent mediation tests.</title> +<link rel="help" href="https://fedidcg.github.io/FedCM"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> + +<script type="module"> +import {request_options_with_two_idps, + fedcm_test, + fedcm_get_and_select_first_account, + fedcm_select_account_promise} from '../support/fedcm-helper.sub.js'; + +fedcm_test(async t => { + const cred = navigator.credentials.get(request_options_with_two_idps('silent')); + return promise_rejects_dom(t, 'NetworkError', cred); +}, "Mediation silent fails if there is no returning account."); + +fedcm_test(async t => { + // Sign in to the first account. + await fedcm_get_and_select_first_account(t, request_options_with_two_idps()); + + // Now use mediation:silent and it should work. + return navigator.credentials.get(request_options_with_two_idps('silent')); +}, "Mediation silent succeeds when there is one returning account."); + +fedcm_test(async t => { + // Sign in to the first account. + await fedcm_get_and_select_first_account(t, request_options_with_two_idps()); + + // Sign in to the second account as well. + let cred = navigator.credentials.get(request_options_with_two_idps()); + fedcm_select_account_promise(t, 1); + await cred; + + // Now use mediation:silent and it should fail. + cred = navigator.credentials.get(request_options_with_two_idps('silent')); + return promise_rejects_dom(t, 'NetworkError', cred); +}, "Mediation silent fails when there is more than one returning account."); +</script> diff --git a/tests/wpt/tests/credential-management/fedcm-multi-idp/single-get-after-onload.https.html b/tests/wpt/tests/credential-management/fedcm-multi-idp/single-get-after-onload.https.html deleted file mode 100644 index de6a7c5371c..00000000000 --- a/tests/wpt/tests/credential-management/fedcm-multi-idp/single-get-after-onload.https.html +++ /dev/null @@ -1,29 +0,0 @@ -<!DOCTYPE html> -<title>Federated Credential Management API multi IDP single get after onload test.</title> -<link rel="help" href="https://fedidcg.github.io/FedCM"> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="/resources/testdriver.js"></script> -<script src="/resources/testdriver-vendor.js"></script> - -<body> - -<script type="module"> -import {set_fedcm_cookie, - request_options_with_mediation_required, - fedcm_get_and_select_first_account} from '../support/fedcm-helper.sub.js'; - -const window_loaded = new Promise(resolve => { - window.addEventListener('load', () => { - resolve(); - }); -}); - -promise_test(async t => { - await set_fedcm_cookie(); - await window_loaded; - const cred = await fedcm_get_and_select_first_account(t, request_options_with_mediation_required()); - assert_equals(cred.token, "token"); -}, "Single `get` call after onload is allowed."); - -</script> diff --git a/tests/wpt/tests/credential-management/fedcm-multi-idp/single-get-before-onload.https.html b/tests/wpt/tests/credential-management/fedcm-multi-idp/single-get-before-onload.https.html deleted file mode 100644 index 0ac9b0e9202..00000000000 --- a/tests/wpt/tests/credential-management/fedcm-multi-idp/single-get-before-onload.https.html +++ /dev/null @@ -1,37 +0,0 @@ -<!DOCTYPE html> -<title>Federated Credential Management API multi IDP single get before onload test.</title> -<link rel="help" href="https://fedidcg.github.io/FedCM"> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="/resources/testdriver.js"></script> -<script src="/resources/testdriver-vendor.js"></script> - -<body> - -<script type="module"> -import {set_fedcm_cookie, - request_options_with_mediation_required, - fedcm_select_account_promise} from '../support/fedcm-helper.sub.js'; - -let has_window_loaded = false; -const window_loaded = new Promise(resolve => { - window.addEventListener('load', () => { - has_window_loaded = true; - resolve(); - }); -}); - -promise_test(async t => { - const first_cred = navigator.credentials.get(request_options_with_mediation_required()); - assert_false(has_window_loaded); - await set_fedcm_cookie(); - await window_loaded; - assert_true(has_window_loaded); - - // Select first account after onload. - await fedcm_select_account_promise(t, 0); - const first = await first_cred; - assert_equals(first.token, "token"); -}, "Single `get` call before onload is allowed even if account is selected after."); - -</script> diff --git a/tests/wpt/tests/credential-management/fedcm-multi-idp/single-get-during-onload.https.html b/tests/wpt/tests/credential-management/fedcm-multi-idp/single-get-during-onload.https.html deleted file mode 100644 index 832565744d4..00000000000 --- a/tests/wpt/tests/credential-management/fedcm-multi-idp/single-get-during-onload.https.html +++ /dev/null @@ -1,30 +0,0 @@ -<!DOCTYPE html> -<title>Federated Credential Management API multi IDP single get during onload test.</title> -<link rel="help" href="https://fedidcg.github.io/FedCM"> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="/resources/testdriver.js"></script> -<script src="/resources/testdriver-vendor.js"></script> - -<body> - -<script type="module"> -import {request_options_with_mediation_required, - set_fedcm_cookie, - fedcm_get_and_select_first_account} from '../support/fedcm-helper.sub.js'; - -promise_test(async t => { - const window_loaded = new Promise(resolve => { - window.addEventListener('load', async () => { - await set_fedcm_cookie(); - const first_cred = fedcm_get_and_select_first_account(t, - request_options_with_mediation_required()); - const cred = await first_cred; - assert_equals(cred.token, "token"); - resolve(); - }); - }); - await window_loaded; -}, "Single `get` call during onload is allowed."); - -</script> diff --git a/tests/wpt/tests/credential-management/fedcm-same-site-none/fedcm-same-site-none.https.html b/tests/wpt/tests/credential-management/fedcm-same-site-none/fedcm-same-site-none.https.html index 77ecdaff9fe..d3d20ea9df2 100644 --- a/tests/wpt/tests/credential-management/fedcm-same-site-none/fedcm-same-site-none.https.html +++ b/tests/wpt/tests/credential-management/fedcm-same-site-none/fedcm-same-site-none.https.html @@ -20,6 +20,6 @@ fedcm_test(async t => { const cred = await fedcm_get_and_select_first_account(t, options); assert_equals(cred.token, "token"); assert_equals(cred.isAutoSelected, false); -}, "FedCM requests should be considered cross-origin and therefore not send SameSite=Strict cookies."); +}, "FedCM requests should be considered cross-origin and therefore not send SameSite=Strict or Lax cookies."); </script> diff --git a/tests/wpt/tests/credential-management/support/fedcm-helper.sub.js b/tests/wpt/tests/credential-management/support/fedcm-helper.sub.js index f0031fa531a..308950e1e29 100644 --- a/tests/wpt/tests/credential-management/support/fedcm-helper.sub.js +++ b/tests/wpt/tests/credential-management/support/fedcm-helper.sub.js @@ -1,6 +1,7 @@ export const manifest_origin = "https://{{host}}:{{ports[https][0]}}"; export const alt_manifest_origin = 'https://{{hosts[alt][]}}:{{ports[https][0]}}'; export const same_site_manifest_origin = 'https://{{hosts[][www1]}}:{{ports[https][0]}}'; +export const default_manifest_path = '/credential-management/support/fedcm/manifest.py'; export function open_and_wait_for_popup(origin, path) { return new Promise(resolve => { @@ -100,6 +101,25 @@ credential-management/support/fedcm/${manifest_filename}`; }; } +export function request_options_with_two_idps(mediation = 'required') { + const first_config = `${manifest_origin}${default_manifest_path}`; + const second_config = `${alt_manifest_origin}${default_manifest_path}`; + return { + identity: { + providers: [{ + configURL: first_config, + clientId: '123', + nonce: 'N1' + }, + { + configURL: second_config, + clientId: '456', + nonce: 'N2' + }], + }, + mediation: mediation + }; +} // Test wrapper which does FedCM-specific setup. export function fedcm_test(test_func, test_name) { diff --git a/tests/wpt/tests/credential-management/support/fedcm/accounts_check_same_site_strict.py b/tests/wpt/tests/credential-management/support/fedcm/accounts_check_same_site_strict.py index a6f385feac1..796ac003cbc 100644 --- a/tests/wpt/tests/credential-management/support/fedcm/accounts_check_same_site_strict.py +++ b/tests/wpt/tests/credential-management/support/fedcm/accounts_check_same_site_strict.py @@ -7,6 +7,8 @@ def main(request, response): return request_error if request.cookies.get(b"same_site_strict") == b"1": return (546, [], "Should not send SameSite=Strict cookies") + if request.cookies.get(b"same_site_lax") == b"1": + return (547, [], "Should not send SameSite=Lax cookies") if request.headers.get(b"Sec-Fetch-Site") != b"cross-site": return (538, [], "Wrong Sec-Fetch-Site header") diff --git a/tests/wpt/tests/credential-management/support/fedcm/token_check_same_site_strict.py b/tests/wpt/tests/credential-management/support/fedcm/token_check_same_site_strict.py index 8a4b3a234bd..4e55bf27f61 100644 --- a/tests/wpt/tests/credential-management/support/fedcm/token_check_same_site_strict.py +++ b/tests/wpt/tests/credential-management/support/fedcm/token_check_same_site_strict.py @@ -7,6 +7,8 @@ def main(request, response): return request_error if request.cookies.get(b"same_site_strict") == b"1": return (546, [], "Should not send SameSite=Strict cookies") + if request.cookies.get(b"same_site_lax") == b"1": + return (547, [], "Should not send SameSite=Lax cookies") response.headers.set(b"Content-Type", b"application/json") response.headers.set(b"Access-Control-Allow-Origin", request.headers.get(b"Origin")) diff --git a/tests/wpt/tests/credential-management/support/set_cookie.headers b/tests/wpt/tests/credential-management/support/set_cookie.headers index ddeb0bb3d6d..df223115a7f 100644 --- a/tests/wpt/tests/credential-management/support/set_cookie.headers +++ b/tests/wpt/tests/credential-management/support/set_cookie.headers @@ -1,3 +1,4 @@ Content-Type: text/html Set-Cookie: cookie=1; SameSite=None; Secure; Path=/ Set-Cookie: same_site_strict=1; SameSite=Strict; Secure; Path=/ +Set-Cookie: same_site_lax=1; SameSite=Lax; Secure; Path=/ diff --git a/tests/wpt/tests/css/css-anchor-position/chrome-336164421-crash.html b/tests/wpt/tests/css/css-anchor-position/chrome-336164421-crash.html new file mode 100644 index 00000000000..c45b69059bd --- /dev/null +++ b/tests/wpt/tests/css/css-anchor-position/chrome-336164421-crash.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<link rel="help" href="https://crbug.com/336164421"> +<style> + #inner { + position: absolute; + left: anchor(left); + } + #inner::before { + display: none; + content: ""; + } +</style> +<div id="inner"></div> diff --git a/tests/wpt/tests/css/css-anchor-position/chrome-336322507-crash.html b/tests/wpt/tests/css/css-anchor-position/chrome-336322507-crash.html new file mode 100644 index 00000000000..922c53b9774 --- /dev/null +++ b/tests/wpt/tests/css/css-anchor-position/chrome-336322507-crash.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<link rel="help" href="https://crbug.com/336322507"> +<style> + #crash { + --grad: linear-gradient(black, white); + background: var(--grad); + position: absolute; + top: anchor(center); + } +</style> +<div id="crash"></div> +<script> + document.body.offsetTop; + crash.remove(); +</script> diff --git a/tests/wpt/tests/css/css-anchor-position/position-visibility-anchors-visible-stacked-child.tentative.html b/tests/wpt/tests/css/css-anchor-position/position-visibility-anchors-visible-stacked-child.tentative.html new file mode 100644 index 00000000000..7c0d5dc6aad --- /dev/null +++ b/tests/wpt/tests/css/css-anchor-position/position-visibility-anchors-visible-stacked-child.tentative.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<meta name="assert" content="Position-visibility: anchors-visible should hide an element and stacked children with an out-of-view anchor." /> +<title>CSS Anchor Positioning Test: position-visibility: anchors-visible</title> +<link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7758"> +<link rel="match" href="position-visibility-anchors-visible-ref.html"> +<style> + #scroll-container { + overflow: hidden scroll; + width: 300px; + height: 100px; + } + + #anchor { + anchor-name: --a1; + width: 100px; + height: 100px; + background: orange; + } + + #spacer { + height: 100px; + } + + #target { + position-anchor: --a1; + position-visibility: anchors-visible; + inset-area: bottom right; + width: 100px; + height: 100px; + background: red; + position: absolute; + top: 0; + left: 0; + } + #stacking-child { + /* stacking context */ + z-index: 1; + width: 100px; + height: 100px; + background: maroon; + position: absolute; + top: 25px; + left: 25px; + } +</style> + +<div id="scroll-container"> + <div id="anchor">anchor</div> + <div id="spacer"></div> + <div id="target">target + <div id="stacking-child"></div> + </div> +</div> + +<script> + const scroller = document.getElementById('scroll-container'); + scroller.scrollTop = 100; + // #target should not be visible because #anchor is scrolled out of view. +</script> diff --git a/tests/wpt/tests/css/css-animations/parsing/animation-delay-end-computed.tentative.html b/tests/wpt/tests/css/css-animations/parsing/animation-delay-end-computed.tentative.html deleted file mode 100644 index 77f96706383..00000000000 --- a/tests/wpt/tests/css/css-animations/parsing/animation-delay-end-computed.tentative.html +++ /dev/null @@ -1,12 +0,0 @@ -<!DOCTYPE html> -<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#propdef-animation-delay-end"> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="/css/support/computed-testcommon.js"></script> -<div id="target"></div> -<script> -test_computed_value("animation-delay-end", "initial", "0s"); -test_computed_value("animation-delay-end", "-500ms", "-0.5s"); -test_computed_value("animation-delay-end", "calc(2 * 3s)", "6s"); -test_computed_value("animation-delay-end", "20s, 10s"); -</script> diff --git a/tests/wpt/tests/css/css-animations/parsing/animation-delay-end-invalid.tentative.html b/tests/wpt/tests/css/css-animations/parsing/animation-delay-end-invalid.tentative.html deleted file mode 100644 index 7cabd4e8e54..00000000000 --- a/tests/wpt/tests/css/css-animations/parsing/animation-delay-end-invalid.tentative.html +++ /dev/null @@ -1,29 +0,0 @@ -<!DOCTYPE html> -<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#propdef-animation-delay-end"> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="/css/support/parsing-testcommon.js"></script> -<script> -test_invalid_value("animation-delay-end", "infinite"); -test_invalid_value("animation-delay-end", "0"); -test_invalid_value("animation-delay-end", "1s 2s"); -test_invalid_value("animation-delay-end", "1s / 2s"); -test_invalid_value("animation-delay-end", "100px"); -test_invalid_value("animation-delay-end", "100%"); - -test_invalid_value("animation-delay-end", "peek 50%"); -test_invalid_value("animation-delay-end", "50% contain"); -test_invalid_value("animation-delay-end", "50% cover"); -test_invalid_value("animation-delay-end", "50% entry"); -test_invalid_value("animation-delay-end", "50% enter"); -test_invalid_value("animation-delay-end", "50% exit"); -test_invalid_value("animation-delay-end", "contain contain"); -test_invalid_value("animation-delay-end", "auto"); -test_invalid_value("animation-delay-end", "none"); -test_invalid_value("animation-delay-end", "cover 50% enter 50%"); -test_invalid_value("animation-delay-end", "cover 100px"); -test_invalid_value("animation-delay-end", "cover"); -test_invalid_value("animation-delay-end", "contain"); -test_invalid_value("animation-delay-end", "enter"); -test_invalid_value("animation-delay-end", "exit"); -</script> diff --git a/tests/wpt/tests/css/css-animations/parsing/animation-delay-end-valid.tentative.html b/tests/wpt/tests/css/css-animations/parsing/animation-delay-end-valid.tentative.html deleted file mode 100644 index 162c781bb01..00000000000 --- a/tests/wpt/tests/css/css-animations/parsing/animation-delay-end-valid.tentative.html +++ /dev/null @@ -1,11 +0,0 @@ -<!DOCTYPE html> -<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#propdef-animation-delay-end"> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="/css/support/parsing-testcommon.js"></script> -<script> -test_valid_value("animation-delay-end", "-5ms"); -test_valid_value("animation-delay-end", "0s"); -test_valid_value("animation-delay-end", "10s"); -test_valid_value("animation-delay-end", "20s, 10s"); -</script> diff --git a/tests/wpt/tests/css/css-animations/parsing/animation-delay-shorthand-computed.html b/tests/wpt/tests/css/css-animations/parsing/animation-delay-shorthand-computed.html deleted file mode 100644 index 0a1eb96041c..00000000000 --- a/tests/wpt/tests/css/css-animations/parsing/animation-delay-shorthand-computed.html +++ /dev/null @@ -1,16 +0,0 @@ -<!DOCTYPE html> -<title>animation-delay shorthand (computed values)</title> -<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#propdef-animation-delay"> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="/css/support/computed-testcommon.js"></script> -<div id="target"></div> -<script> -test_computed_value("animation-delay", "1s"); -test_computed_value("animation-delay", "-1s"); -test_computed_value("animation-delay", "1s 2s"); -test_computed_value("animation-delay", "1s, 2s"); -test_computed_value("animation-delay", "1s 2s, 3s"); -test_computed_value("animation-delay", "1s, 2s 3s"); -test_computed_value("animation-delay", "1s, 2s, 3s"); -</script> diff --git a/tests/wpt/tests/css/css-animations/parsing/animation-delay-shorthand.html b/tests/wpt/tests/css/css-animations/parsing/animation-delay-shorthand.html deleted file mode 100644 index 5c74a4d8e43..00000000000 --- a/tests/wpt/tests/css/css-animations/parsing/animation-delay-shorthand.html +++ /dev/null @@ -1,49 +0,0 @@ -<!DOCTYPE html> -<title>animation-delay shorthand</title> -<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#propdef-animation-delay"> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="/css/support/shorthand-testcommon.js"></script> -<script src="/css/support/parsing-testcommon.js"></script> -<script> -test_valid_value("animation-delay", "1s"); -test_valid_value("animation-delay", "-1s"); -test_valid_value("animation-delay", "1s 2s"); -test_valid_value("animation-delay", "1s, 2s"); -test_valid_value("animation-delay", "1s 2s, 3s"); -test_valid_value("animation-delay", "1s, 2s 3s"); -test_valid_value("animation-delay", "1s, 2s, 3s"); - -test_invalid_value("animation-delay", "1s 2s 3s"); -test_invalid_value("animation-delay", "0s, 1s 2s 3s"); -test_invalid_value("animation-delay", "1s / 2s"); -test_invalid_value("animation-delay", "1s, 2px"); -test_invalid_value("animation-delay", "#ff0000"); -test_invalid_value("animation-delay", "red"); -test_invalid_value("animation-delay", "thing"); -test_invalid_value("animation-delay", "thing 0%"); -test_invalid_value("animation-delay", "thing 42%"); -test_invalid_value("animation-delay", "thing 100%"); -test_invalid_value("animation-delay", "thing 100px"); -test_invalid_value("animation-delay", "100% thing"); - -test_shorthand_value('animation-delay', '1s 2s', { - 'animation-delay-start': '1s', - 'animation-delay-end': '2s', -}); - -test_shorthand_value('animation-delay', '1s', { - 'animation-delay-start': '1s', - 'animation-delay-end': '0s', -}); - -test_shorthand_value('animation-delay', '1s 2s, 3s 4s', { - 'animation-delay-start': '1s, 3s', - 'animation-delay-end': '2s, 4s', -}); - -test_shorthand_value('animation-delay', '1s 2s, 3s, 4s 5s', { - 'animation-delay-start': '1s, 3s, 4s', - 'animation-delay-end': '2s, 0s, 5s', -}); -</script> diff --git a/tests/wpt/tests/css/css-animations/parsing/animation-delay-start-computed.tentative.html b/tests/wpt/tests/css/css-animations/parsing/animation-delay-start-computed.tentative.html deleted file mode 100644 index bfb89d0267f..00000000000 --- a/tests/wpt/tests/css/css-animations/parsing/animation-delay-start-computed.tentative.html +++ /dev/null @@ -1,12 +0,0 @@ -<!DOCTYPE html> -<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#propdef-animation-delay-start"> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="/css/support/computed-testcommon.js"></script> -<div id="target"></div> -<script> -test_computed_value("animation-delay-start", "initial", "0s"); -test_computed_value("animation-delay-start", "-500ms", "-0.5s"); -test_computed_value("animation-delay-start", "calc(2 * 3s)", "6s"); -test_computed_value("animation-delay-start", "20s, 10s"); -</script> diff --git a/tests/wpt/tests/css/css-animations/parsing/animation-delay-start-invalid.tentative.html b/tests/wpt/tests/css/css-animations/parsing/animation-delay-start-invalid.tentative.html deleted file mode 100644 index bff31f3789a..00000000000 --- a/tests/wpt/tests/css/css-animations/parsing/animation-delay-start-invalid.tentative.html +++ /dev/null @@ -1,29 +0,0 @@ -<!DOCTYPE html> -<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#propdef-animation-delay-start"> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="/css/support/parsing-testcommon.js"></script> -<script> -test_invalid_value("animation-delay-start", "infinite"); -test_invalid_value("animation-delay-start", "0"); -test_invalid_value("animation-delay-start", "1s 2s"); -test_invalid_value("animation-delay-start", "1s / 2s"); -test_invalid_value("animation-delay-start", "100px"); -test_invalid_value("animation-delay-start", "100%"); - -test_invalid_value("animation-delay-start", "peek 50%"); -test_invalid_value("animation-delay-start", "50% contain"); -test_invalid_value("animation-delay-start", "50% cover"); -test_invalid_value("animation-delay-start", "50% entry"); -test_invalid_value("animation-delay-start", "50% enter"); -test_invalid_value("animation-delay-start", "50% exit"); -test_invalid_value("animation-delay-start", "contain contain"); -test_invalid_value("animation-delay-start", "auto"); -test_invalid_value("animation-delay-start", "none"); -test_invalid_value("animation-delay-start", "cover 50% enter 50%"); -test_invalid_value("animation-delay-start", "cover 100px"); -test_invalid_value("animation-delay-start", "cover"); -test_invalid_value("animation-delay-start", "contain"); -test_invalid_value("animation-delay-start", "enter"); -test_invalid_value("animation-delay-start", "exit"); -</script> diff --git a/tests/wpt/tests/css/css-animations/parsing/animation-delay-start-valid.tentative.html b/tests/wpt/tests/css/css-animations/parsing/animation-delay-start-valid.tentative.html deleted file mode 100644 index f52286444ed..00000000000 --- a/tests/wpt/tests/css/css-animations/parsing/animation-delay-start-valid.tentative.html +++ /dev/null @@ -1,11 +0,0 @@ -<!DOCTYPE html> -<link rel="help" href="https://drafts.csswg.org/scroll-animations-1/#propdef-animation-delay-start"> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="/css/support/parsing-testcommon.js"></script> -<script> -test_valid_value("animation-delay-start", "-5ms"); -test_valid_value("animation-delay-start", "0s"); -test_valid_value("animation-delay-start", "10s"); -test_valid_value("animation-delay-start", "20s, 10s"); -</script> diff --git a/tests/wpt/tests/css/css-break/ruby-002.html b/tests/wpt/tests/css/css-break/ruby-002.html index d17cc563f38..2c4f6aae5ba 100644 --- a/tests/wpt/tests/css/css-break/ruby-002.html +++ b/tests/wpt/tests/css/css-break/ruby-002.html @@ -30,15 +30,13 @@ <div style="position:relative; width:100px; height:100px; background:red;"> <div style="columns:2; column-fill:auto; column-gap:0; height:175px; orphans:1; widows:1;"> <ruby> - <div class="main"></div><rt><div class="annotation"></div></rt> - </ruby><ruby class="under"> - <div class="main"></div><rt><div class="annotation"></div></rt> + <div class="main"></div><rt><div class="annotation"></div></rt></ruby + ><ruby class="under"><div class="main"></div><rt><div class="annotation"></div></rt> </ruby> <br> <ruby> - <div class="main"></div><rt><div class="annotation"></div></rt> - </ruby><ruby class="under"> - <div class="main"></div><rt><div class="annotation"></div></rt> + <div class="main"></div><rt><div class="annotation"></div></rt></ruby + ><ruby class="under"><div class="main"></div><rt><div class="annotation"></div></rt> </ruby> <br> </div> diff --git a/tests/wpt/tests/css/css-cascade/all-prop-revert-layer.html b/tests/wpt/tests/css/css-cascade/all-prop-revert-layer.html index 868267b2855..3a1d621d174 100644 --- a/tests/wpt/tests/css/css-cascade/all-prop-revert-layer.html +++ b/tests/wpt/tests/css/css-cascade/all-prop-revert-layer.html @@ -15,8 +15,6 @@ alt: "a"; animation-composition: add; animation-delay: 123s; - animation-delay-start: 123s; - animation-delay-end: 456s; animation-direction: reverse; animation-duration: 123s; animation-fill-mode: both; diff --git a/tests/wpt/tests/css/css-cascade/scope-pseudo-element-ref.html b/tests/wpt/tests/css/css-cascade/scope-pseudo-element-ref.html new file mode 100644 index 00000000000..2ad6a0995a9 --- /dev/null +++ b/tests/wpt/tests/css/css-cascade/scope-pseudo-element-ref.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<title>@scope - pseudo-elements (ref)</title> +<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scoped-styles"> +<link rel="help" href="https://drafts.csswg.org/selectors-4/#link"> + +<!-- Cosmetic --> +<style> + body > div { + display: inline-block; + width: 100px; + height: 100px; + border: 1px solid black; + vertical-align:top; + } +</style> + +<!-- ::before --> +<style> + #before_test > main { + background-color: skyblue; + } + #before_test > main::before { + content: "B"; + width: 20px; + height: 20px; + display: inline-block; + background-color: tomato; + } +</style> +<div id=before_test> + <main> + Foo + </main> +</div> + +<!-- ::after --> +<style> + #after_test > main { + background-color: skyblue; + } + #after_test > main::after { + content: "A"; + width: 20px; + height: 20px; + display: inline-block; + background-color: tomato; + } +</style> +<div id=after_test> + <main> + Foo + </main> +</div> + +<!-- ::marker --> +<style> + #marker_test li { + background-color: skyblue; + } + #marker_test li::marker { + content: "M"; + } +</style> +<div id=marker_test> + <ul> + <li>One</li> + <li>Two</li> + </ul> +</div> diff --git a/tests/wpt/tests/css/css-cascade/scope-pseudo-element.html b/tests/wpt/tests/css/css-cascade/scope-pseudo-element.html new file mode 100644 index 00000000000..29c44690609 --- /dev/null +++ b/tests/wpt/tests/css/css-cascade/scope-pseudo-element.html @@ -0,0 +1,76 @@ +<!DOCTYPE html> +<title>@scope - pseudo-elements</title> +<link rel="help" href="https://drafts.csswg.org/css-cascade-6/#scoped-styles"> +<link rel="help" href="https://drafts.csswg.org/selectors-4/#link"> +<link rel="match" href="scope-pseudo-element-ref.html"> + +<!-- Cosmetic --> +<style> + body > div { + display: inline-block; + width: 100px; + height: 100px; + border: 1px solid black; + vertical-align:top; + } +</style> + +<!-- ::before --> +<style> + @scope (#before_test > main) { + :scope { + background-color: skyblue; + } + :scope::before { + content: "B"; + width: 20px; + height: 20px; + display: inline-block; + background-color: tomato; + } + } +</style> +<div id=before_test> + <main> + Foo + </main> +</div> + +<!-- ::after --> +<style> + @scope (#after_test > main) { + :scope { + background-color: skyblue; + } + :scope::after { + content: "A"; + width: 20px; + height: 20px; + display: inline-block; + background-color: tomato; + } + } +</style> +<div id=after_test> + <main> + Foo + </main> +</div> + +<!-- ::marker --> +<style> + @scope (#marker_test li) { + :scope { + background-color: skyblue; + } + :scope::marker { + content: "M"; + } + } +</style> +<div id=marker_test> + <ul> + <li>One</li> + <li>Two</li> + </ul> +</div> diff --git a/tests/wpt/tests/css/css-color-adjust/rendering/dark-color-scheme/WEB_FEATURES.yml b/tests/wpt/tests/css/css-color-adjust/rendering/dark-color-scheme/WEB_FEATURES.yml new file mode 100644 index 00000000000..48bbe25901e --- /dev/null +++ b/tests/wpt/tests/css/css-color-adjust/rendering/dark-color-scheme/WEB_FEATURES.yml @@ -0,0 +1,3 @@ +features: +- name: color-scheme + files: "**" diff --git a/tests/wpt/tests/css/css-color-adjust/rendering/dark-color-scheme/support/prefers-color-scheme.svg b/tests/wpt/tests/css/css-color-adjust/rendering/dark-color-scheme/support/prefers-color-scheme.svg deleted file mode 100644 index 3afcac70fe9..00000000000 --- a/tests/wpt/tests/css/css-color-adjust/rendering/dark-color-scheme/support/prefers-color-scheme.svg +++ /dev/null @@ -1,10 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" width='100px' height='100px'> - <style> - @media (prefers-color-scheme: dark) { - rect { - fill: green; - } - } - </style> - <rect fill='blue' width='100px' height='100px' /> -</svg> diff --git a/tests/wpt/tests/css/css-color-adjust/rendering/dark-color-scheme/svg-as-image.html b/tests/wpt/tests/css/css-color-adjust/rendering/dark-color-scheme/svg-as-image.html deleted file mode 100644 index 6fc33f56cea..00000000000 --- a/tests/wpt/tests/css/css-color-adjust/rendering/dark-color-scheme/svg-as-image.html +++ /dev/null @@ -1,8 +0,0 @@ -<!doctype html> -<head> - <title>prefers-color-scheme inside an SVG image</title> - <link rel="help" href="https://www.w3.org/TR/mediaqueries-5/#descdef-media-prefers-color-scheme"> - <link rel="match" href="svg-as-image-ref.html"> -</head> -<p>There should be green square below when the preferred color-scheme is dark, and blue otherwise.</p> -<img src='support/prefers-color-scheme.svg'> diff --git a/tests/wpt/tests/css/css-color/t424-hsl-clip-outside-gamut-b-ref.html b/tests/wpt/tests/css/css-color/hsl-clamp-negative-saturation-ref.html index 7d1f1bf2786..33a82517ae7 100644 --- a/tests/wpt/tests/css/css-color/t424-hsl-clip-outside-gamut-b-ref.html +++ b/tests/wpt/tests/css/css-color/hsl-clamp-negative-saturation-ref.html @@ -7,8 +7,6 @@ td { border: none; padding: 0; height: 1.2em; } </style> <body> - <p><strong>WARNING: This test assumes that the device gamut is sRGB - (as it will be for many CRT monitors).</strong></p> <p>Every row in this table should have both columns the same color:</p> <table> <tr> @@ -16,21 +14,6 @@ <th style="background:black; color: white">Column 2</th> </tr> <tr> - <td colspan='2' style="background: black"> </td> - </tr> - <tr> - <td colspan='2' style="background: black"> </td> - </tr> - <tr> - <td colspan='2' style="background: white"> </td> - </tr> - <tr> - <td colspan='2' style="background: rgb(0, 0, 255)"> </td> - </tr> - <tr> - <td colspan='2' style="background: rgb(102, 0, 255)"> </td> - </tr> - <tr> <td colspan='2' style="background: rgb(102, 102, 102)"> </td> </tr> <tr> diff --git a/tests/wpt/tests/css/css-color/hsl-clamp-negative-saturation.html b/tests/wpt/tests/css/css-color/hsl-clamp-negative-saturation.html new file mode 100644 index 00000000000..25b54c2e2cb --- /dev/null +++ b/tests/wpt/tests/css/css-color/hsl-clamp-negative-saturation.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<html> + <head> + <title>CSS Test: hsl() clamp negative saturation values</title> + <link rel="author" title="L. David Baron" href="https://dbaron.org/" /> + <link rel="author" title="Mozilla Corporation" href="http://mozilla.com/" /> + <link rel="help" href="https://www.w3.org/TR/css-color-4/#the-hsl-notation" /> + <link rel="match" href="hsl-clamp-negative-saturation-ref.html" /> + <meta name="assert" content="Test clamping of negative saturation values in hsl() functions." /> + <style type="text/css"> + + table { border-spacing: 0 2px; padding: 0; border: none; } + td { border: none; padding: 0; height: 1.2em; } + + </style> + </head> + <body> + + <p>Every row in this table should have both columns the same color:</p> + + <table> + <tr> + <th style="background:white; color: black">Column 1</th> + <th style="background:black; color: white">Column 2</th> + </tr> + <tr> + <td style="background: hsl(0, -50%, 40%)"> </td> + <td style="background: rgb(102, 102, 102)"> </td> + </tr> + <tr> + <td style="background: hsl(30, -50%, 60%)"> </td> + <td style="background: rgb(153, 153, 153)"> </td> + </tr> + </table> + + </body> +</html> diff --git a/tests/wpt/tests/css/css-color/t425-hsla-clip-outside-device-gamut-b-ref.html b/tests/wpt/tests/css/css-color/hsla-clamp-negative-saturation-ref.html index d35a608fb58..bbf5c3b037d 100644 --- a/tests/wpt/tests/css/css-color/t425-hsla-clip-outside-device-gamut-b-ref.html +++ b/tests/wpt/tests/css/css-color/hsla-clamp-negative-saturation-ref.html @@ -8,8 +8,6 @@ td { border: none; padding: 0; height: 1.2em; } </style> <body> - <p><strong>WARNING: This test assumes that the device gamut is sRGB - (as it will be for many CRT monitors).</strong></p> <p>Every row in this table should have both columns the same color:</p> <table> <tr> @@ -17,21 +15,6 @@ <th style="background:black; color: white">Column 2</th> </tr> <tr> - <td colspan='2' style="background: rgb(102, 102, 102)"> </td> - </tr> - <tr> - <td colspan='2' style="background: rgb(153, 153, 153)"> </td> - </tr> - <tr> - <td colspan='2' style="background: white"> </td> - </tr> - <tr> - <td colspan='2' style="background: rgb(153, 153, 255)"> </td> - </tr> - <tr> - <td colspan='2' style="background: rgb(163, 102, 255)"> </td> - </tr> - <tr> <td colspan='2' style="background: rgb(194, 194, 194)"> </td> </tr> <tr> diff --git a/tests/wpt/tests/css/css-color/hsla-clamp-negative-saturation.html b/tests/wpt/tests/css/css-color/hsla-clamp-negative-saturation.html new file mode 100644 index 00000000000..239151efe3a --- /dev/null +++ b/tests/wpt/tests/css/css-color/hsla-clamp-negative-saturation.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<html> + <head> + <title>CSS Test: hsla() clamp negative saturation values</title> + <link rel="author" title="L. David Baron" href="https://dbaron.org/" /> + <link rel="author" title="Mozilla Corporation" href="http://mozilla.com/" /> + <link rel="help" href="https://www.w3.org/TR/css-color-4/#the-hsl-notation" /> + <link rel="match" href="hsla-clamp-negative-saturation-ref.html" /> + <meta name="assert" content="Test clamping of negative saturation values in hsla() functions." /> + <style type="text/css"> + + body { background: white; color: black; } + table { border-spacing: 0 2px; padding: 0; border: none; } + td { border: none; padding: 0; height: 1.2em; } + + </style> + </head> + <body> + + <p>Every row in this table should have both columns the same color:</p> + + <table> + <tr> + <th style="background:white; color: black">Column 1</th> + <th style="background:black; color: white">Column 2</th> + </tr> + <tr> + <td style="background: hsla(0, -50%, 40%, 0.4)"> </td> + <td style="background: rgb(194, 194, 194)"> </td> + </tr> + <tr> + <td style="background: hsla(30, -50%, 60%, 0.6)"> </td> + <td style="background: rgb(194, 194, 194)"> </td> + </tr> + </table> + + </body> +</html> diff --git a/tests/wpt/tests/css/css-color/t424-hsl-clip-outside-gamut-b.xht b/tests/wpt/tests/css/css-color/t424-hsl-clip-outside-gamut-b.xht deleted file mode 100644 index d66c2db925a..00000000000 --- a/tests/wpt/tests/css/css-color/t424-hsl-clip-outside-gamut-b.xht +++ /dev/null @@ -1,60 +0,0 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml"> - <head> - <title>CSS Test: hsl() clipping outside device gamut</title> - <link rel="author" title="L. David Baron" href="https://dbaron.org/" /> - <link rel="author" title="Mozilla Corporation" href="http://mozilla.com/" /> - <link rel="help" href="http://www.w3.org/TR/css3-color/#hsl-color" /> - <link rel="match" href="t424-hsl-clip-outside-gamut-b-ref.html" /> - <meta name="assert" content="Test clipping of hsl() values outside the device gamut." /> - <style type="text/css"><![CDATA[ - - table { border-spacing: 0 2px; padding: 0; border: none; } - td { border: none; padding: 0; height: 1.2em; } - - ]]></style> - </head> - <body> - - <p><strong>WARNING: This test assumes that the device gamut is sRGB - (as it will be for many CRT monitors).</strong></p> - - <p>Every row in this table should have both columns the same color:</p> - - <table> - <tr> - <th style="background:white; color: black">Column 1</th> - <th style="background:black; color: white">Column 2</th> - </tr> - <tr> - <td style="background: hsl(240, 100%, -100%)"> </td> - <td style="background: black"> </td> - </tr> - <tr> - <td style="background: hsl(240, 75%, -20%)"> </td> - <td style="background: black"> </td> - </tr> - <tr> - <td style="background: hsl(240, 75%, 120%)"> </td> - <td style="background: white"> </td> - </tr> - <tr> - <td style="background: hsl(240, 130%, 50%)"> </td> - <td style="background: rgb(0, 0, 255)"> </td> - </tr> - <tr> - <td style="background: hsl(264, 130%, 50%)"> </td> - <td style="background: rgb(102, 0, 255)"> </td> - </tr> - <tr> - <td style="background: hsl(0, -50%, 40%)"> </td> - <td style="background: rgb(102, 102, 102)"> </td> - </tr> - <tr> - <td style="background: hsl(30, -50%, 60%)"> </td> - <td style="background: rgb(153, 153, 153)"> </td> - </tr> - </table> - - </body> -</html> diff --git a/tests/wpt/tests/css/css-color/t425-hsla-clip-outside-device-gamut-b.xht b/tests/wpt/tests/css/css-color/t425-hsla-clip-outside-device-gamut-b.xht deleted file mode 100644 index d30cf8c4839..00000000000 --- a/tests/wpt/tests/css/css-color/t425-hsla-clip-outside-device-gamut-b.xht +++ /dev/null @@ -1,61 +0,0 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml"> - <head> - <title>CSS Test: hsla() clipping outside device gamut</title> - <link rel="author" title="L. David Baron" href="https://dbaron.org/" /> - <link rel="author" title="Mozilla Corporation" href="http://mozilla.com/" /> - <link rel="help" href="http://www.w3.org/TR/css3-color/#hsla-color" /> - <link rel="match" href="t425-hsla-clip-outside-device-gamut-b-ref.html" /> - <meta name="assert" content="Test clipping of hsla() values outside the device gamut." /> - <style type="text/css"><![CDATA[ - - body { background: white; color: black; } - table { border-spacing: 0 2px; padding: 0; border: none; } - td { border: none; padding: 0; height: 1.2em; } - - ]]></style> - </head> - <body> - - <p><strong>WARNING: This test assumes that the device gamut is sRGB - (as it will be for many CRT monitors).</strong></p> - - <p>Every row in this table should have both columns the same color:</p> - - <table> - <tr> - <th style="background:white; color: black">Column 1</th> - <th style="background:black; color: white">Column 2</th> - </tr> - <tr> - <td style="background: hsla(240, 100%, -100%, 0.6)"> </td> - <td style="background: rgb(102, 102, 102)"> </td> - </tr> - <tr> - <td style="background: hsla(240, 75%, -20%, 0.4)"> </td> - <td style="background: rgb(153, 153, 153)"> </td> - </tr> - <tr> - <td style="background: hsla(240, 75%, 120%, 0.6)"> </td> - <td style="background: white"> </td> - </tr> - <tr> - <td style="background: hsla(240, 130%, 50%, 0.4)"> </td> - <td style="background: rgb(153, 153, 255)"> </td> - </tr> - <tr> - <td style="background: hsla(264, 130%, 50%, 0.6)"> </td> - <td style="background: rgb(163, 102, 255)"> </td> - </tr> - <tr> - <td style="background: hsla(0, -50%, 40%, 0.4)"> </td> - <td style="background: rgb(194, 194, 194)"> </td> - </tr> - <tr> - <td style="background: hsla(30, -50%, 60%, 0.6)"> </td> - <td style="background: rgb(194, 194, 194)"> </td> - </tr> - </table> - - </body> -</html> diff --git a/tests/wpt/tests/css/css-contain/contain-layout-baseline-005.html b/tests/wpt/tests/css/css-contain/contain-layout-baseline-005.html index 0971402e6b9..fb706cbf030 100644 --- a/tests/wpt/tests/css/css-contain/contain-layout-baseline-005.html +++ b/tests/wpt/tests/css/css-contain/contain-layout-baseline-005.html @@ -43,7 +43,6 @@ fieldset, details { <div class="wrapper"> <canvas></canvas> <div class="inline-block">foo</div> - <button>foo</button> <select><option>foo</option></select> <select multiple style="height: 40px;"><option>foo</option></select> <textarea style="height: 40px;"></textarea> diff --git a/tests/wpt/tests/css/css-contain/contain-layout-button-001.html b/tests/wpt/tests/css/css-contain/contain-layout-button-001.tentative.html index b53b28879e2..f5a664e566f 100644 --- a/tests/wpt/tests/css/css-contain/contain-layout-button-001.html +++ b/tests/wpt/tests/css/css-contain/contain-layout-button-001.tentative.html @@ -5,7 +5,7 @@ <link rel="help" href="https://drafts.csswg.org/css-contain-1/#containment-layout"> <link rel="help" href="https://drafts.csswg.org/css2/visudet.html#propdef-vertical-align"> <link rel="match" href="reference/contain-layout-button-001-ref.html"> -<meta name=assert content="Layout containment does apply to buttons, thus their baseline is their margin-bottom edge."> +<meta name=assert content="Layout containment does apply to buttons, but in an inline context their baseline is synthesized from their content-box."> <style> button { border: 5px solid green; diff --git a/tests/wpt/tests/css/css-contain/contain-layout-button-002.tentative.html b/tests/wpt/tests/css/css-contain/contain-layout-button-002.tentative.html new file mode 100644 index 00000000000..15026d55f8c --- /dev/null +++ b/tests/wpt/tests/css/css-contain/contain-layout-button-002.tentative.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<link rel="help" href="https://drafts.csswg.org/css-contain-1/#containment-layout"> +<link rel="help" href="https://github.com/web-platform-tests/wpt/issues/45889"> +<link rel="match" href="reference/contain-layout-button-002-ref.html"> +<meta name=assert content="An empty button and a button with layout containment should align the same."> +<style> +button { + border: 5px solid green; + padding: 10px; + margin-bottom: 2px; + color: transparent; + width: 20px; + height: 20px; +} +</style> + +<p>This test passes if it has the same output as the reference.</p> +<button></button> <button style="contain:layout"></button> diff --git a/tests/wpt/tests/css/css-contain/content-visibility/content-visibility-background-clip-crash.html b/tests/wpt/tests/css/css-contain/content-visibility/content-visibility-background-clip-crash.html new file mode 100644 index 00000000000..1fa645f4577 --- /dev/null +++ b/tests/wpt/tests/css/css-contain/content-visibility/content-visibility-background-clip-crash.html @@ -0,0 +1,23 @@ +<!doctype html> +<html> +<body> + <style> + body { + content-visibility: hidden; + background-clip: text; + position: absolute; + border-right-style: dashed; + border-top-style: ridge; + background-color: green; + } + </style> + <script> + var p1 = document.createElement('p'); + var p2 = document.createElement('p'); + document.body.appendChild(p1); + document.body.appendChild(p2); + p1.scroll(); + p2.remove(); + </script> +</body> +</html> diff --git a/tests/wpt/tests/css/css-contain/reference/contain-layout-baseline-005-ref.html b/tests/wpt/tests/css/css-contain/reference/contain-layout-baseline-005-ref.html index 360652c9397..83340137ac6 100644 --- a/tests/wpt/tests/css/css-contain/reference/contain-layout-baseline-005-ref.html +++ b/tests/wpt/tests/css/css-contain/reference/contain-layout-baseline-005-ref.html @@ -39,7 +39,6 @@ fieldset, details { <div class="wrapper"> <canvas></canvas> <div class="inline-block">foo</div> - <button>foo</button> <select><option>foo</option></select> <select multiple style="height: 40px;"><option>foo</option></select> <textarea style="height: 40px;"></textarea> diff --git a/tests/wpt/tests/css/css-contain/reference/contain-layout-button-001-ref.html b/tests/wpt/tests/css/css-contain/reference/contain-layout-button-001-ref.html index da83204dce6..c20efbbb39d 100644 --- a/tests/wpt/tests/css/css-contain/reference/contain-layout-button-001-ref.html +++ b/tests/wpt/tests/css/css-contain/reference/contain-layout-button-001-ref.html @@ -7,7 +7,7 @@ div.fakeButton { display: inline-block; border: 5px solid green; padding: 0; - margin-bottom: 2px; + margin-bottom: -5px; color: transparent; width: 0; height: 0px; diff --git a/tests/wpt/tests/css/css-contain/reference/contain-layout-button-002-ref.html b/tests/wpt/tests/css/css-contain/reference/contain-layout-button-002-ref.html new file mode 100644 index 00000000000..85b98ee4a66 --- /dev/null +++ b/tests/wpt/tests/css/css-contain/reference/contain-layout-button-002-ref.html @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<style> +button { + border: 5px solid green; + padding: 10px; + margin-bottom: 2px; + color: transparent; + width: 20px; + height: 20px; +} +</style> +<p>This test passes if it has the same output as the reference.</p> +<button></button> <button></button> diff --git a/tests/wpt/tests/css/css-display/accessibility/display-contents-role-and-label.html b/tests/wpt/tests/css/css-display/accessibility/display-contents-role-and-label.html index 4a06d0ff5bf..6b5453e9f64 100644 --- a/tests/wpt/tests/css/css-display/accessibility/display-contents-role-and-label.html +++ b/tests/wpt/tests/css/css-display/accessibility/display-contents-role-and-label.html @@ -75,7 +75,7 @@ </thead> <tbody> <tr style="display: contents;"> - <td data-expectedrole="cell" data-expectedlabel="x" data-testname="td as child of tr with display: contents, within table with display: flex, has cell role" class="ex-role-and-label">x</td> + <td data-expectedrole="cell" data-testname="td as child of tr with display: contents, within table with display: flex, has cell role" class="ex-role">x</td> <td>x</td> </tr> </tbody> @@ -90,7 +90,7 @@ </thead> <tbody> <tr style="display: contents;"> - <td data-expectedrole="cell" data-expectedlabel="x" data-testname="td as child of tr with display: contents, within table with role=table with display: flex, has cell role" class="ex-role-and-label">x</td> + <td data-expectedrole="cell" data-testname="td as child of tr with display: contents, within table with role=table with display: flex, has cell role" class="ex-role">x</td> <td>x</td> </tr> </tbody> @@ -105,7 +105,7 @@ </thead> <tbody> <tr style="display: contents;"> - <td data-expectedrole="gridcell" data-expectedlabel="x" data-testname="td (no explicit role) as child of tr with display: contents, within table with role=grid with display: flex, has gridcell role" class="ex-role-and-label">x</td> + <td data-expectedrole="gridcell" data-testname="td (no explicit role) as child of tr with display: contents, within table with role=grid with display: flex, has gridcell role" class="ex-role">x</td> <td>x</td> </tr> </tbody> @@ -116,7 +116,7 @@ <h2>x</h2> <ul style="display: contents" data-expectedrole="list" data-testname="ul with display: contents, as child of div with display: grid, has list role" class="ex-role"> <li>x</li> - <li data-expectedrole="listitem" data-expectedlabel="x" data-testname="listitem within ul with display: contents, as child of div with display: grid, has listitem role" class="ex-role-and-label">x</li> + <li data-expectedrole="listitem" data-testname="listitem within ul with display: contents, as child of div with display: grid, has listitem role" class="ex-role">x</li> </ul> </div> @@ -131,8 +131,8 @@ <!-- Landmarks and regions --> <header style="display: contents;" data-expectedrole="banner" data-testname="header with display: contents has banner role" class="ex-role">x</header> - <nav style="display: contents;" aria-label="label" data-expectedrole="navigation" data-testname="nav with display: contents and aria-label has navigation role" class="ex-role-and-label">x</nav> - <aside style="display: contents;" aria-label="label" data-expectedrole="complementary" data-testname="aside with display: contents and aria-label has complementary role" class="ex-role-and-label">x</aside> + <nav style="display: contents;" aria-label="label" data-expectedrole="navigation" data-expectedlabel="label" data-testname="nav with display: contents and aria-label has navigation role" class="ex-role-and-label">x</nav> + <aside style="display: contents;" aria-label="label" data-expectedrole="complementary" data-expectedlabel="label" data-testname="aside with display: contents and aria-label has complementary role" class="ex-role-and-label">x</aside> <main style="display: contents;" data-expectedrole="main" data-testname="main with display: contents has main role" class="ex-role">x</main> <footer style="display: contents;" data-expectedrole="contentinfo" data-testname="footer with display: contents has contentinfo role" class="ex-role">x</footer> <form aria-label="label" style="display: contents;" data-expectedrole="form" data-expectedlabel="label" data-testname="form with display: contents has form role" class="ex-role-and-label">x</form> @@ -140,12 +140,12 @@ <section aria-label="label" style="display: contents;" data-expectedrole="region" data-expectedlabel="label" data-testname="section with aria-label and display: contents has region role" class="ex-role-and-label"></section> <div role="banner" style="display: contents;" data-expectedrole="banner" data-testname="div with role banner and display: contents has banner role" class="ex-role">x</div> - <div role="navigation" aria-label="label" style="display: contents;" data-expectedrole="navigation" data-testname="div with role navigation, aria-label and display: contents has navigation role" class="ex-role-and-label">x</div> - <div role="complementary" aria-label="label" style="display: contents;" data-expectedrole="complementary" data-testname="div with role complementary, aria-label and display: contents has complementary role" class="ex-role-and-label">x</div> + <div role="navigation" aria-label="label" style="display: contents;" data-expectedrole="navigation" data-expectedlabel="label" data-testname="div with role navigation, aria-label and display: contents has navigation role" class="ex-role-and-label">x</div> + <div role="complementary" aria-label="label" style="display: contents;" data-expectedrole="complementary" data-expectedlabel="label" data-testname="div with role complementary, aria-label and display: contents has complementary role" class="ex-role-and-label">x</div> <div role="main" style="display: contents;" data-expectedrole="main" data-testname="div with role main and display: contents has main role" class="ex-role">x</div> <div role="contentinfo" style="display: contents;" data-expectedrole="contentinfo" data-testname="div with role contentinfo and display: contents has contentinfo role" class="ex-role">x</div> <div role="form" aria-label="label" style="display: contents;" data-expectedrole="form" data-expectedlabel="label" data-testname="div with role form, aria-label and display: contents has form role" class="ex-role-and-label">x</div> - <div role="search" aria-label="label" style="display: contents;" data-expectedrole="search" data-testname="div with role search and display: contents has search role" class="ex-role-and-label">x</div> + <div role="search" aria-label="label" style="display: contents;" data-expectedrole="search" data-expectedlabel="label" data-testname="div with role search and display: contents has search role" class="ex-role-and-label">x</div> <div role="region" aria-label="label" style="display: contents;" data-expectedrole="region" data-expectedlabel="label" data-testname="div with role region, aria-label and display: contents has region role" class="ex-role-and-label">x</div> <!-- Links --> @@ -156,33 +156,33 @@ <!-- Lists --> <ul role="list" style="display: contents;" data-expectedrole="list" data-testname="ul with role list and display: contents (child li has display: contents) has list role" class="ex-role"> - <li style="display: contents;" data-expectedrole="listitem" data-expectedlabel="x" data-testname="li as child of ul with role list, both with display: contents, has listitem role" class="ex-role-and-label">x</li> + <li style="display: contents;" data-expectedrole="listitem" data-testname="li as child of ul with role list, both with display: contents, has listitem role" class="ex-role">x</li> <li>y</li> </ul> <ul role="list" style="display: contents;" data-expectedrole="list" data-testname="ul with role list and display: contents has list role" class="ex-role"> <li>x</li> - <li data-expectedrole="listitem" data-expectedlabel="y" data-testname="li, as child of ul with role list and display: contents, has listitem role" class="ex-role-and-label">y</li> + <li data-expectedrole="listitem" data-testname="li, as child of ul with role list and display: contents, has listitem role" class="ex-role">y</li> </ul> <ul role="list"> <li>x</li> - <li style="display: contents;" data-expectedrole="listitem" data-expectedlabel="y" data-testname="li with display: contents, as child of ul with role list, has listitem role" class="ex-role-and-label">y</li> + <li style="display: contents;" data-expectedrole="listitem" data-testname="li with display: contents, as child of ul with role list, has listitem role" class="ex-role">y</li> </ul> <ol role="list" style="display: contents;" data-expectedrole="list" data-testname="ol with role list and display: contents has list role (child li has display: contents)" class="ex-role"> - <li style="display: contents;" data-expectedrole="listitem" data-expectedlabel="x" data-testname="li as child of ol with role list, both with display: contents, has listitem role" class="ex-role-and-label">x</li> + <li style="display: contents;" data-expectedrole="listitem" data-testname="li as child of ol with role list, both with display: contents, has listitem role" class="ex-role">x</li> <li>y</li> </ol> <ol role="list" style="display: contents;" data-expectedrole="list" data-testname="ol with role list and display: contents has list role" class="ex-role"> <li>x</li> - <li data-expectedrole="listitem" data-expectedlabel="y" data-testname="li, as child of ol with role list and display: contents, has listitem role" class="ex-role-and-label">y</li> + <li data-expectedrole="listitem" data-testname="li, as child of ol with role list and display: contents, has listitem role" class="ex-role">y</li> </ol> <ol role="list"> <li>x</li> - <li style="display: contents;" data-expectedrole="listitem" data-expectedlabel="y" data-testname="li with display: contents, as child of ol with role list, has listitem role" class="ex-role-and-label">y</li> + <li style="display: contents;" data-expectedrole="listitem" data-testname="li with display: contents, as child of ol with role list, has listitem role" class="ex-role">y</li> </ol> <div role="list" style="display: contents;" data-expectedrole="list" data-testname="div with list role and display: contents has list role" class="ex-role"> @@ -212,7 +212,7 @@ </thead> <tbody> <tr> - <td style="display: contents;" data-expectedrole="cell" data-expectedlabel="x" data-testname="td within tr in table with role table, all with display: contents, has cell role" class="ex-role-and-label">x</td> + <td style="display: contents;" data-expectedrole="cell" data-testname="td within tr in table with role table, all with display: contents, has cell role" class="ex-role">x</td> <td>x</td> </tr> </tbody> @@ -242,7 +242,7 @@ </thead> <tbody> <tr> - <td style="display: contents;" data-expectedrole="gridcell" data-expectedlabel="x" data-testname="td within table with role grid, both with display: contents, has gridcell role" class="ex-role-and-label">x</td> + <td style="display: contents;" data-expectedrole="gridcell" data-testname="td within table with role grid, both with display: contents, has gridcell role" class="ex-role">x</td> <td>x</td> </tr> </tbody> @@ -282,10 +282,10 @@ <div role="treegrid" style="display: contents;" data-expectedrole="treegrid" data-testname="div with role treegrid and display: contents has treegrid role" class="ex-role"></div> <ul role="tree" style="display: contents;" data-expectedrole="tree" data-testname="ul with role tree and display: contents has tree role" class="ex-role"> - <li role="treeitem" aria-expanded="true" style="display: contents;" data-expectedrole="treeitem" data-expectedlabel="x" data-testname="li with role treeitem and display: contents, within ul with role tree and display: contents, has treeitem role" class="ex-role-and-label"> + <li role="treeitem" aria-expanded="true" style="display: contents;" data-expectedrole="treeitem" data-testname="li with role treeitem and display: contents, within ul with role tree and display: contents, has treeitem role" class="ex-role"> <span>x</span> <ul role="group"> - <li role="treeitem" aria-expanded="false" style="display: contents;" data-expectedrole="treeitem" data-expectedlabel="x" data-testname="li with role treeitem and display: contents, within ul with role=group (within ul with role tree and display: contents), has treeitem role" class="ex-role-and-label"> + <li role="treeitem" aria-expanded="false" style="display: contents;" data-expectedrole="treeitem" data-testname="li with role treeitem and display: contents, within ul with role=group (within ul with role tree and display: contents), has treeitem role" class="ex-role"> <span>x</span> </li> </ul> diff --git a/tests/wpt/tests/css/css-easing/linear-timing-functions-syntax.tentative.html b/tests/wpt/tests/css/css-easing/linear-timing-functions-syntax.tentative.html index 99b680d0bde..d2aa4f45b1a 100644 --- a/tests/wpt/tests/css/css-easing/linear-timing-functions-syntax.tentative.html +++ b/tests/wpt/tests/css/css-easing/linear-timing-functions-syntax.tentative.html @@ -34,6 +34,9 @@ test_computed_value("animation-timing-function", "linear(0 calc(min(50%, 60%)), test_computed_value("animation-timing-function", "linear(0 0% 50%, 1 50% 100%)", "linear(0 0%, 0 50%, 1 50%, 1 100%)"); test_computed_value("animation-timing-function", "linear(0, 0.5 25% 75%, 1 100% 100%)", "linear(0 0%, 0.5 25%, 0.5 75%, 1 100%, 1 100%)"); test_computed_value("animation-timing-function", "linear(0, 1.3, 1, 0.92, 1, 0.99, 1, 0.998, 1 100% 100%)", "linear(0 0%, 1.3 12.5%, 1 25%, 0.92 37.5%, 1 50%, 0.99 62.5%, 1 75%, 0.998 87.5%, 1 100%, 1 100%)"); + +test_computed_value("animation-timing-function", "linear(0, 0 40%, 1, 0.5, 1)", "linear(0 0%, 0 40%, 1 60%, 0.5 80%, 1 100%)"); + </script> </body> </html> diff --git a/tests/wpt/tests/css/css-flexbox/intrinsic-size/col-wrap-020.html b/tests/wpt/tests/css/css-flexbox/intrinsic-size/col-wrap-020.html new file mode 100644 index 00000000000..ffbfa711c31 --- /dev/null +++ b/tests/wpt/tests/css/css-flexbox/intrinsic-size/col-wrap-020.html @@ -0,0 +1,34 @@ +<!DOCTYPE html> +<link rel="author" title="David Grogan" href="mailto:dgrogan@chromium.org"> +<link rel="help" href="https://drafts.csswg.org/css-flexbox/#intrinsic-sizes"> +<link rel="match" href="../../reference/ref-filled-green-100px-square.xht"> +<meta name="assert" + content="column-wrap container's max-content width includes gap" /> + +<style> + #reference-overlapped-red { + position: absolute; + background-color: red; + width: 100px; + height: 100px; + z-index: -1; + } + + .item { + /* Remove min-height so we don't have to think about it. */ + min-height: 0px; + width: 10px; + flex: 0 0 100px; + } +</style> + +<p>Test passes if there is a filled green square and <strong>no red</strong>. +</p> + +<div id=reference-overlapped-red></div> + +<div + style="display: flex; flex-flow: column wrap; height: 100px; width: max-content; column-gap: 80px; background: green;"> + <div class="item"></div> + <div class="item"></div> +</div> diff --git a/tests/wpt/tests/css/css-fonts/matching/font-unicode-PUA-primary-font-notref.html b/tests/wpt/tests/css/css-fonts/matching/font-unicode-PUA-primary-font-notref.html new file mode 100644 index 00000000000..df9b829a419 --- /dev/null +++ b/tests/wpt/tests/css/css-fonts/matching/font-unicode-PUA-primary-font-notref.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<head> +<link rel="author" title="Vitor Roriz" href="https://github.com/vitorroriz"> +<link rel="help" href="https://drafts.csswg.org/css-fonts-4/#char-handling-issues"> +<style> +.target { + font-family: Times; + font-size: 60px; +} +</style> +</head> +<body> +"If a given character is a Private-Use Area Unicode codepoint, user agents must only match font families named in the font-family list that are not generic families. If none of the families named in the font-family list contain a glyph for that codepoint, user agents must display some form of missing glyph symbol for that character rather than attempting installed font fallback for that codepoint." - <a href="https://drafts.csswg.org/css-fonts-4/#char-handling-issues">css-fonts-4</a> +<p class="target"></p> +</body> +</html> diff --git a/tests/wpt/tests/css/css-fonts/matching/font-unicode-PUA-primary-font.html b/tests/wpt/tests/css/css-fonts/matching/font-unicode-PUA-primary-font.html new file mode 100644 index 00000000000..1b03c98e403 --- /dev/null +++ b/tests/wpt/tests/css/css-fonts/matching/font-unicode-PUA-primary-font.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<html> +<head> +<link rel="author" title="Vitor Roriz" href="https://github.com/vitorroriz"> +<link rel="help" href="https://drafts.csswg.org/css-fonts-4/#char-handling-issues"> +<link rel="mismatch" href="./font-unicode-PUA-primary-font-notref.html"> +<style> +.target { + font-family: Arial; + font-size: 60px; +} +</style> +</head> +<body> +"If a given character is a Private-Use Area Unicode codepoint, user agents must only match font families named in the font-family list that are not generic families. If none of the families named in the font-family list contain a glyph for that codepoint, user agents must display some form of missing glyph symbol for that character rather than attempting installed font fallback for that codepoint." - <a href="https://drafts.csswg.org/css-fonts-4/#char-handling-issues">css-fonts-4</a> +<p class="target"></p> +</html> diff --git a/tests/wpt/tests/css/css-fonts/resources/vs/MPLUS1-Regular_without-cmap14-subset.ttf b/tests/wpt/tests/css/css-fonts/resources/vs/MPLUS1-Regular_without-cmap14-subset.ttf Binary files differnew file mode 100644 index 00000000000..b77096690df --- /dev/null +++ b/tests/wpt/tests/css/css-fonts/resources/vs/MPLUS1-Regular_without-cmap14-subset.ttf diff --git a/tests/wpt/tests/css/css-fonts/resources/vs/NotoColorEmoji-Regular_subset.ttf b/tests/wpt/tests/css/css-fonts/resources/vs/NotoColorEmoji-Regular_subset.ttf Binary files differnew file mode 100644 index 00000000000..24ab79fd052 --- /dev/null +++ b/tests/wpt/tests/css/css-fonts/resources/vs/NotoColorEmoji-Regular_subset.ttf diff --git a/tests/wpt/tests/css/css-fonts/resources/vs/NotoEmoji-Regular_subset.ttf b/tests/wpt/tests/css/css-fonts/resources/vs/NotoEmoji-Regular_subset.ttf Binary files differnew file mode 100644 index 00000000000..0b054c7c8d5 --- /dev/null +++ b/tests/wpt/tests/css/css-fonts/resources/vs/NotoEmoji-Regular_subset.ttf diff --git a/tests/wpt/tests/css/css-fonts/resources/vs/NotoEmoji-Regular_without-cmap14-subset.ttf b/tests/wpt/tests/css/css-fonts/resources/vs/NotoEmoji-Regular_without-cmap14-subset.ttf Binary files differnew file mode 100644 index 00000000000..3d00ef81dea --- /dev/null +++ b/tests/wpt/tests/css/css-fonts/resources/vs/NotoEmoji-Regular_without-cmap14-subset.ttf diff --git a/tests/wpt/tests/css/css-fonts/resources/vs/NotoSansJP-Regular_with-cmap14-subset.ttf b/tests/wpt/tests/css/css-fonts/resources/vs/NotoSansJP-Regular_with-cmap14-subset.ttf Binary files differnew file mode 100644 index 00000000000..edcb98e3745 --- /dev/null +++ b/tests/wpt/tests/css/css-fonts/resources/vs/NotoSansJP-Regular_with-cmap14-subset.ttf diff --git a/tests/wpt/tests/css/css-fonts/resources/vs/NotoSansMath-Regular_without-cmap14-subset.ttf b/tests/wpt/tests/css/css-fonts/resources/vs/NotoSansMath-Regular_without-cmap14-subset.ttf Binary files differnew file mode 100644 index 00000000000..5436d06dae5 --- /dev/null +++ b/tests/wpt/tests/css/css-fonts/resources/vs/NotoSansMath-Regular_without-cmap14-subset.ttf diff --git a/tests/wpt/tests/css/css-fonts/resources/vs/STIXTwoMath-Regular_with-cmap14-subset.ttf b/tests/wpt/tests/css/css-fonts/resources/vs/STIXTwoMath-Regular_with-cmap14-subset.ttf Binary files differnew file mode 100644 index 00000000000..c9720cf9ce7 --- /dev/null +++ b/tests/wpt/tests/css/css-fonts/resources/vs/STIXTwoMath-Regular_with-cmap14-subset.ttf diff --git a/tests/wpt/tests/css/css-fonts/support/css/variation-sequences.css b/tests/wpt/tests/css/css-fonts/support/css/variation-sequences.css new file mode 100644 index 00000000000..5977f17b678 --- /dev/null +++ b/tests/wpt/tests/css/css-fonts/support/css/variation-sequences.css @@ -0,0 +1,38 @@ +@font-face { + font-family: "MonoEmojiFont"; + src: url(../../resources/vs/NotoEmoji-Regular_subset.ttf); +} + +@font-face { + font-family: "ColorEmojiFont"; + src: url(../../resources/vs/NotoColorEmoji-Regular_subset.ttf); +} + +@font-face { + font-family: "EmojiFontWithBaseCharOnly"; + src: url(../../resources/vs/NotoEmoji-Regular_without-cmap14-subset.ttf); +} + +@font-face { + font-family: "CJKFontWithVS"; + src: url(../../resources/vs/NotoSansJP-Regular_with-cmap14-subset.ttf); +} + +@font-face { + font-family: "CJKFontWithBaseCharOnly"; + src: url(../../resources/vs/MPLUS1-Regular_without-cmap14-subset.ttf); +} + +@font-face { + font-family: "MathFontWithVS"; + src: url(../../resources/vs/STIXTwoMath-Regular_with-cmap14-subset.ttf); +} + +@font-face { + font-family: "MathFontWithBaseCharOnly"; + src: url(../../resources/vs/NotoSansMath-Regular_without-cmap14-subset.ttf); +} + +body { + font-size: 24px; +}
\ No newline at end of file diff --git a/tests/wpt/tests/css/css-fonts/support/js/variation-sequences.js b/tests/wpt/tests/css/css-fonts/support/js/variation-sequences.js new file mode 100644 index 00000000000..84c5a1a9c77 --- /dev/null +++ b/tests/wpt/tests/css/css-fonts/support/js/variation-sequences.js @@ -0,0 +1,125 @@ +var baseChars = { + "emoji": "\u{1fae8}", + "cjk": "\u{8279}", + "math": "\u{2205}" +}; + +var variationSelectors = { + "emoji": ["\u{fe0e}", "\u{fe0f}"], + "cjk": ["", "\u{FE00}", "\u{FE01}", "\u{e0100}", "\u{e0101}", + "\u{e0102}" + ], + "math": ["", "\u{FE00}"] +}; + +var families = { + "emoji": ["ColorEmojiFont", "MonoEmojiFont", + "EmojiFontWithBaseCharOnly", + "sans-serif" + ], + "cjk": ["CJKFontWithVS", "CJKFontWithBaseCharOnly", + "sans-serif" + ], + "math": ["MathFontWithVS", "MathFontWithBaseCharOnly", + "sans-serif" + ] +}; + +var variationSequenceFamilies = new Map([ + ["\u{1fae8}\u{fe0e}", "MonoEmojiFont"], + ["\u{1fae8}\u{fe0f}", "ColorEmojiFont"], + ["\u{8279}\u{fe00}", "CJKFontWithVS"], + ["\u{8279}\u{fe01}", "CJKFontWithVS"], + ["\u{8279}\u{e0100}", "CJKFontWithVS"], + ["\u{8279}\u{e0101}", "CJKFontWithVS"], + ["\u{8279}\u{e0102}", "CJKFontWithVS"], + ["\u{2205}\u{FE00}", "MathFontWithVS"] +]); + +var baseCharFamilies = new Map([ + ["\u{1fae8}", new Set(["MonoEmojiFont", "ColorEmojiFont", + "EmojiFontWithBaseCharOnly" + ])], + ["\u{8279}", new Set(["CJKFontWithVS", + "CJKFontWithBaseCharOnly" + ])], + ["\u{2205}", new Set(["MathFontWithVS", + "MathFontWithBaseCharOnly" + ])] +]); + +const range = function*(l) { + for (let i = 0; i < l; i += 1) yield i; +} +const isEmpty = arr => + arr.length === 0; + +const permutations = + function*(a) { + const r = arguments[1] || []; + if (isEmpty(a)) + yield r; + for (let i of range(a.length)) { + const aa = [...a]; + const rr = [...r, ...aa.splice(i, 1)]; + yield* permutations(aa, rr); + } +} + +function getMatchedFamilyForVariationSequence( + familyList, baseCharacter, variationSelector) { + const variationSequence = baseCharacter + variationSelector; + // First try to find a match for the whole variation sequence. + if (variationSequenceFamilies.has(variationSequence)) { + const matchedFamily = variationSequenceFamilies.get(variationSequence); + if (familyList.includes(matchedFamily)) { + return matchedFamily; + } + } + // If failed, try to match only the base character from the + // variation sequence. + if (baseCharFamilies.has(baseCharacter)) { + const eligibleFamilies = baseCharFamilies.get(baseCharacter); + const matchedFamilies = + familyList.filter(value => eligibleFamilies.has(value)); + if (matchedFamilies.length) { + return matchedFamilies[0]; + } + } + // We should not reach here, we should always match one of the + // specified web fonts in the tests. + return ""; +} + +function generateContent( + families, baseChar, variationSelectors, getFontFamilyValue) { + var rootElem = document.createElement('div'); + // We want to test all possible combinations of variation + // selectors and font-family list values. For the refs, + // we explicitly specify the font that we expect to be + // matched from the maps at the beginning of the files. + const allFamiliesLists = permutations(families); + for (const familyList of allFamiliesLists) { + for (const variationSelector of variationSelectors) { + const contentSpan = document.createElement("span"); + contentSpan.textContent = baseChar + variationSelector; + contentSpan.style.fontFamily = + getFontFamilyValue(familyList, baseChar, variationSelector); + rootElem.appendChild(contentSpan); + } + } + document.body.appendChild(rootElem); +} + +function generateVariationSequenceTests(type) { + var getFontFamilyValue = (familyList, baseChar, variationSelector) => { + return familyList.join(', '); + } + generateContent(families[type], baseChars[type], variationSelectors[type], getFontFamilyValue); +} + +function generateVariationSequenceRefs(type) { + generateContent( + families[type], baseChars[type], variationSelectors[type], + getMatchedFamilyForVariationSequence); +} diff --git a/tests/wpt/tests/css/css-fonts/variation-sequences-ref.html b/tests/wpt/tests/css/css-fonts/variation-sequences-ref.html new file mode 100644 index 00000000000..a44f18bb3e1 --- /dev/null +++ b/tests/wpt/tests/css/css-fonts/variation-sequences-ref.html @@ -0,0 +1,11 @@ +<!DOCTYPE html> +<meta charset="UTF-8" /> +<title>CSS Test: Cluster Matching Variation Sequences</title> +<link rel="stylesheet" type="text/css" href="support/css/variation-sequences.css" /> +<script type="text/javascript" src="support/js/variation-sequences.js"></script> +<body></body> +<script> + generateVariationSequenceRefs("emoji"); + generateVariationSequenceRefs("cjk"); + generateVariationSequenceRefs("math"); +</script>
\ No newline at end of file diff --git a/tests/wpt/tests/css/css-fonts/variation-sequences.html b/tests/wpt/tests/css/css-fonts/variation-sequences.html new file mode 100644 index 00000000000..91e46a84d71 --- /dev/null +++ b/tests/wpt/tests/css/css-fonts/variation-sequences.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<meta charset="UTF-8" /> +<title>CSS Test: Cluster Matching Variation Sequences</title> +<link rel="help" href="https://www.w3.org/TR/css-fonts-4/#cluster-matching" /> +<link rel="help" href="https://unicode.org/reports/tr51/" /> +<link rel="help" href="https://unicode.org/reports/tr37/" /> +<link rel="help" href="https://www.unicode.org/Public/UNIDATA/StandardizedVariants.txt" /> +<link rel="help" href="https://www.unicode.org/versions/Unicode15.1.0/ch23.pdf#G19053" /> +<link rel="match" href="variation-sequences-ref.html"> +<meta name="assert" content="Variation sequences should be taken into account during cluster matching."> +<link rel="stylesheet" type="text/css" href="support/css/variation-sequences.css" /> +<script type="text/javascript" src="support/js/variation-sequences.js"></script> +<body></body> +<script> + generateVariationSequenceTests("emoji"); + generateVariationSequenceTests("cjk"); + generateVariationSequenceTests("math"); +</script>
\ No newline at end of file diff --git a/tests/wpt/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-check-grid-height-on-resize-ref.html b/tests/wpt/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-check-grid-height-on-resize-ref.html index 71968864924..53447e0dad8 100644 --- a/tests/wpt/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-check-grid-height-on-resize-ref.html +++ b/tests/wpt/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-check-grid-height-on-resize-ref.html @@ -1,16 +1,14 @@ <!DOCTYPE html> <html> - +<title>CSS Reference: Masonry grid container size changes when item sizes change</title> <head> <meta charset="utf-8"> <link rel="author" title="Brandon Stewart" href="mailto:brandonstewart@apple.com"> -<link rel="help" href="https://drafts.csswg.org/css-grid-3"> -<meta name="assert" content="When the height of an element in the grid changes, ensure the grid is properly resized"> </head> <style> grid { - display: grid; + display: inline-grid; grid-template-rows: masonry; grid-template-columns: auto; grid-gap: 10px; @@ -20,7 +18,7 @@ item1 { background-color: grey; height: 125px; - width: 250px; + width: 300px; } item2 { background-color: grey; @@ -35,4 +33,4 @@ <item2>2</item2> </grid> </body> -</html>
\ No newline at end of file +</html> diff --git a/tests/wpt/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-check-grid-height-on-resize.html b/tests/wpt/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-check-grid-height-on-resize.html index 06c2901f27b..dbff19f28e1 100644 --- a/tests/wpt/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-check-grid-height-on-resize.html +++ b/tests/wpt/tests/css/css-grid/masonry/tentative/track-sizing/masonry-track-sizing-check-grid-height-on-resize.html @@ -1,17 +1,18 @@ <!DOCTYPE html> -<html> +<html class="reftest-wait"> <head> <meta charset="utf-8"> +<title>CSS Test: Masonry grid container size changes when item sizes change</title> <link rel="author" title="Brandon Stewart" href="mailto:brandonstewart@apple.com"> <link rel="help" href="https://drafts.csswg.org/css-grid-3"> <link rel="match" href="masonry-track-sizing-check-grid-height-on-resize-ref.html"> -<meta name="assert" content="When the height of an element in the grid changes, ensure the grid is properly resized"> +<meta name="assert" content="Test passes if when the size of an item in the masonry grid changes, the grid container is properly resized"> </head> <style> grid { - display: grid; + display: inline-grid; grid-template-rows: masonry; grid-template-columns: auto; grid-gap: 10px; @@ -32,8 +33,11 @@ </grid> </body> <script> - /* Force a relayout */ - document.body.offsetHeight; - document.querySelector("item").style["height"] = "125px"; + document.body.offsetHeight; // Force a relayout + var gridItem = document.querySelector('item'); + gridItem.style.height = '125px'; + gridItem.style.width = '300px'; + document + document.documentElement.className = ''; </script> -</html>
\ No newline at end of file +</html> diff --git a/tests/wpt/tests/css/css-inline/text-box-trim/text-box-trim-half-leading-block-box-001.html b/tests/wpt/tests/css/css-inline/text-box-trim/text-box-trim-half-leading-block-box-001.html index 2cbf1c26dbf..14429b10022 100644 --- a/tests/wpt/tests/css/css-inline/text-box-trim/text-box-trim-half-leading-block-box-001.html +++ b/tests/wpt/tests/css/css-inline/text-box-trim/text-box-trim-half-leading-block-box-001.html @@ -15,6 +15,6 @@ </style> <div class ="div-parent""> - <div id="d1"></div> + <div id="d2">Testline1<br>Testline2<br>Testline3</div> </div> diff --git a/tests/wpt/tests/css/css-inline/text-box-trim/text-box-trim-half-leading-block-box-002-ref.html b/tests/wpt/tests/css/css-inline/text-box-trim/text-box-trim-half-leading-block-box-002-ref.html new file mode 100644 index 00000000000..8c10a803706 --- /dev/null +++ b/tests/wpt/tests/css/css-inline/text-box-trim/text-box-trim-half-leading-block-box-002-ref.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<title>Reference for trimming block-boxes at their first/last formatted lines</title> +<link rel="help" href="https://drafts.csswg.org/css-inline-3/#leading-trim"> +<link rel="stylesheet" type="text/css" href="/fonts/ahem.css" /> + +<style> +.div-parent { + outline: 1px solid orange; + font-family: Ahem; + font-size: 20px; + line-height: 1; + writing-mode:vertical-lr; +} +</style> +<div class ="div-parent"> + Test<br><br> +</div> diff --git a/tests/wpt/tests/css/css-inline/text-box-trim/text-box-trim-half-leading-block-box-002.html b/tests/wpt/tests/css/css-inline/text-box-trim/text-box-trim-half-leading-block-box-002.html new file mode 100644 index 00000000000..fdf5b71d434 --- /dev/null +++ b/tests/wpt/tests/css/css-inline/text-box-trim/text-box-trim-half-leading-block-box-002.html @@ -0,0 +1,17 @@ +<!DOCTYPE html> +<title>Tests block boxes's edges are trimmed at text-over/text-under baselines of their first/last formatted lines</title> +<link rel="help" href="https://drafts.csswg.org/css-inline-3/#leading-trim"> +<link rel="stylesheet" type="text/css" href="/fonts/ahem.css" /> +<link rel="match" href="text-box-trim-half-leading-block-box-002-ref.html"> + +<style> +.div-parent { + outline: 1px solid orange; + font-family: Ahem; + font-size: 20px; + line-height: 3; + writing-mode:vertical-lr; +} +</style> + +<div class ="div-parent" style="text-box-trim:start">Test</div> diff --git a/tests/wpt/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position-rounding-error-ref.html b/tests/wpt/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position-rounding-error-ref.html index a069e4d3ae5..e9f54f3ad3d 100644 --- a/tests/wpt/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position-rounding-error-ref.html +++ b/tests/wpt/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position-rounding-error-ref.html @@ -1,7 +1,6 @@ <!DOCTYPE html> <style> .container { - position: fixed; width: 70px; height: 126px; background-color: green; diff --git a/tests/wpt/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position-rounding-error.html b/tests/wpt/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position-rounding-error.html index 8a02a5b2a6d..02a14ad65fe 100644 --- a/tests/wpt/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position-rounding-error.html +++ b/tests/wpt/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position-rounding-error.html @@ -2,11 +2,10 @@ <html class="reftest-wait"> <link rel="help" href="https://drafts.csswg.org/css-shapes-1/#basic-shape-interpolation"> <link rel="match" href="clip-path-animation-fixed-position-rounding-error-ref.html"> +<meta name="fuzzy" content="maxDifference=0-64;totalPixels=0-400"> <!-- Test that clip paths on elements with position: fixed draw correctly, even in scenarios that involve partial pixels - - Currently uses fuzzy diff because of crbug.com/1249071 --> <style> .container { diff --git a/tests/wpt/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position.html b/tests/wpt/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position.html index 3ffc2a30a7f..20fc8e52d89 100644 --- a/tests/wpt/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position.html +++ b/tests/wpt/tests/css/css-masking/clip-path/animations/clip-path-animation-fixed-position.html @@ -1,6 +1,6 @@ <!DOCTYPE html> <html class="reftest-wait"> -<meta name=fuzzy content="0-10;0-150"> +<meta name=fuzzy content="0-60;0-350"> <link rel="help" href="https://drafts.csswg.org/css-shapes-1/#basic-shape-interpolation"> <link rel="match" href="clip-path-animation-fixed-position-ref.html"> <!-- diff --git a/tests/wpt/tests/css/css-masking/clip-path/animations/two-clip-path-animation-diff-length4.html b/tests/wpt/tests/css/css-masking/clip-path/animations/two-clip-path-animation-diff-length4.html new file mode 100644 index 00000000000..0a893cb7563 --- /dev/null +++ b/tests/wpt/tests/css/css-masking/clip-path/animations/two-clip-path-animation-diff-length4.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<link rel="help" href="https://drafts.csswg.org/css-shapes-1/#basic-shape-interpolation"> +<link rel="match" href="two-clip-path-animation-diff-length1-ref.html"> +<style> + #container { + width: 100px; + height: 100px; + background-color: green; + animation: clippath2 0.5s; + } + + @keyframes clippath2 { + 0% { + clip-path: inset(10px 10px); + } + + 1% { + clip-path: inset(10px 10px); + } + + 100% { + clip-path: inset(40px 40px); + } + } +</style> +<script src="/common/reftest-wait.js"></script> +<script src="../../../../web-animations/testcommon.js"></script> +<script src="../../../../web-animations/resources/timing-utils.js"></script> + +<body> + <div id="container"></div> + + <script> + // This test ensures that a new no-op animation still invalidates paint + // and the animation running on the compositor. + document.getAnimations()[0].ready.then(() => { + let anim = document.getElementById("container").animate( + [ + { clipPath: "inset(10px 10px)" }, + { clipPath: "inset(10px 10px)" }, + ], + 2000, + ); + anim.ready.then(() => { return requestAnimationFrame(takeScreenshot); }); + }); + </script> +</body> + +</html> diff --git a/tests/wpt/tests/css/css-overflow/scroll-with-ancestor-border-radius.html b/tests/wpt/tests/css/css-overflow/scroll-with-ancestor-border-radius.html new file mode 100644 index 00000000000..9192c613e9e --- /dev/null +++ b/tests/wpt/tests/css/css-overflow/scroll-with-ancestor-border-radius.html @@ -0,0 +1,59 @@ +<!doctype html> +<meta charset=utf-8> +<title>Should not scroll out of rounded corner</title> +<link rel="help" href="https://crbug.com/40277896"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="/dom/events/scrolling/scroll_support.js"></script> +<style> + #container { + width: 300px; + height: 300px; + border-radius: 100px; + overflow: hidden; + border: 2px solid blue; + } + + #scroller { + overflow: auto; + width: 300px; + height: 300px; + will-change: scroll-position; + } + + .spacer { + height: 200vh; + } + +</style> + +<div id="container"> + <div id="scroller"> + <div class="spacer"></div> + </div> +</div> +<div class="spacer"></div> + +<script> + promise_test(async (t) => { + await waitForCompositorCommit(); + let scrolled = new Promise((resolve) => { + let scrollers = [window, document.getElementById("scroller")]; + let onscroll = (evt) => { + for (const scroller of scrollers) { + scroller.removeEventListener("scroll", onscroll); + } + resolve(evt.target.id || "root"); + } + for (const scroller of scrollers) { + scroller.addEventListener("scroll", onscroll); + } + }); + const actions = new test_driver.Actions().scroll(20, 20, 0, 50, { duration: 50 }); + actions.send(); + assert_equals(await scrolled, "root", "Incorrect element scrolled"); + }, "Wheel-scroll out of rounded corner skips that scroller"); +</script> diff --git a/tests/wpt/tests/css/css-overflow/scroll-with-border-radius.html b/tests/wpt/tests/css/css-overflow/scroll-with-border-radius.html new file mode 100644 index 00000000000..88bb0f1fa88 --- /dev/null +++ b/tests/wpt/tests/css/css-overflow/scroll-with-border-radius.html @@ -0,0 +1,51 @@ +<!doctype html> +<meta charset=utf-8> +<title>Should not scroll out of rounded corner</title> +<link rel="help" href="https://crbug.com/40277896"> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="/dom/events/scrolling/scroll_support.js"></script> +<style> + #scroller { + border-radius: 100px; + overflow: auto; + width: 300px; + height: 300px; + border: 2px solid blue; + will-change: scroll-position; + } + + .spacer { + height: 200vh; + } + +</style> + +<div id="scroller"> + <div class="spacer"></div> +</div> +<div class="spacer"></div> + +<script> + promise_test(async (t) => { + await waitForCompositorCommit(); + let scrolled = new Promise((resolve) => { + let scrollers = [window, document.getElementById("scroller")]; + let onscroll = (evt) => { + for (const scroller of scrollers) { + scroller.removeEventListener("scroll", onscroll); + } + resolve(evt.target.id || "root"); + } + for (const scroller of scrollers) { + scroller.addEventListener("scroll", onscroll); + } + }); + const actions = new test_driver.Actions().scroll(20, 20, 0, 50, { duration: 50 }); + actions.send(); + assert_equals(await scrolled, "root", "Incorrect element scrolled"); + }, "Wheel-scroll out of rounded corner skips that scroller"); +</script> diff --git a/tests/wpt/tests/css/css-page/page-box-000-print-ref.html b/tests/wpt/tests/css/css-page/page-box-000-print-ref.html new file mode 100644 index 00000000000..d39bd738507 --- /dev/null +++ b/tests/wpt/tests/css/css-page/page-box-000-print-ref.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<link rel="author" title="Morten Stenshorne" href="mailto:mstensho@chromium.org"> +<style> + html { + box-sizing: border-box; + display: grid; + place-items: center; + height: 100%; + border: 20px solid green; + } +</style> +This page should have a green border. diff --git a/tests/wpt/tests/css/css-page/page-box-000-print.html b/tests/wpt/tests/css/css-page/page-box-000-print.html new file mode 100644 index 00000000000..aee317ab975 --- /dev/null +++ b/tests/wpt/tests/css/css-page/page-box-000-print.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<link rel="author" title="Morten Stenshorne" href="mailto:mstensho@chromium.org"> +<link rel="help" href="https://drafts.csswg.org/css-page-3/#page-properties"> +<link rel="match" href="page-box-000-print-ref.html"> +<style> + @page { + border: 20px solid green; + } + html { + display: grid; + place-items: center; + height: 100%; + } +</style> +This page should have a green border. diff --git a/tests/wpt/tests/css/css-position/sticky/position-sticky-left-004.html b/tests/wpt/tests/css/css-position/sticky/position-sticky-left-004.html new file mode 100644 index 00000000000..79188e84b0d --- /dev/null +++ b/tests/wpt/tests/css/css-position/sticky/position-sticky-left-004.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>CSS Position Test: Test position:sticky element with 100% left in a block container</title> +<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com"> +<link rel="author" title="Mozilla" href="https://www.mozilla.org/"> +<link rel="help" href="https://drafts.csswg.org/css-position/#sticky-pos"> +<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1748891"> +<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1835705"> +<link rel="match" href="../../reference/ref-filled-green-100px-square.xht"> +<meta name="assert" content="This test verifies that a position:sticky element with 100% left value does not cause the scrollbar to show when shrinking the width of its overflow container."> + +<style> +#scroll { + width: 200px; + height: 100px; + overflow: auto; + background: red; +} +.sticky { + position: sticky; + width: 100px; + height: 100px; + left: 100%; + background: green; +} +</style> + +<p>Test passes if there is a filled green square and <strong>no red</strong>.</p> +<div id="scroll"> + <div class="sticky"></div> +</div> + +<script> +window.onload = () => { + document.getElementById("scroll").style.width = "100px"; +}; +</script> diff --git a/tests/wpt/tests/css/css-position/sticky/position-sticky-left-005.html b/tests/wpt/tests/css/css-position/sticky/position-sticky-left-005.html new file mode 100644 index 00000000000..b029e593635 --- /dev/null +++ b/tests/wpt/tests/css/css-position/sticky/position-sticky-left-005.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>CSS Position Test: Test position:sticky element with 100% left in a grid container</title> +<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com"> +<link rel="author" title="Mozilla" href="https://www.mozilla.org/"> +<link rel="help" href="https://drafts.csswg.org/css-position/#sticky-pos"> +<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1748891"> +<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1835705"> +<link rel="match" href="../../reference/ref-filled-green-100px-square.xht"> +<meta name="assert" content="This test verifies that a position:sticky element with 100% left value does not cause the scrollbar to show when shrinking the width of its overflow container."> + +<style> +#scroll { + display: grid; + width: 200px; + height: 100px; + overflow: auto; + background: red; +} +.sticky { + position: sticky; + width: 100px; + height: 100px; + left: 100%; + background: green; +} +</style> + +<p>Test passes if there is a filled green square and <strong>no red</strong>.</p> +<div id="scroll"> + <div class="sticky"></div> +</div> + +<script> +window.onload = () => { + document.getElementById("scroll").style.width = "100px"; +}; +</script> diff --git a/tests/wpt/tests/css/css-position/sticky/position-sticky-left-006.html b/tests/wpt/tests/css/css-position/sticky/position-sticky-left-006.html new file mode 100644 index 00000000000..63fa1ebfd89 --- /dev/null +++ b/tests/wpt/tests/css/css-position/sticky/position-sticky-left-006.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>CSS Position Test: Test position:sticky element with 100% left in a flex container</title> +<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com"> +<link rel="author" title="Mozilla" href="https://www.mozilla.org/"> +<link rel="help" href="https://drafts.csswg.org/css-position/#sticky-pos"> +<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1748891"> +<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1835705"> +<link rel="match" href="../../reference/ref-filled-green-100px-square.xht"> +<meta name="assert" content="This test verifies that a position:sticky element with 100% left value does not cause the scrollbar to show when shrinking the width of its overflow container."> + +<style> +#scroll { + display: flex; + width: 200px; + height: 100px; + overflow: auto; + background: red; +} +.sticky { + position: sticky; + width: 100px; + height: 100px; + left: 100%; + background: green; +} +</style> + +<p>Test passes if there is a filled green square and <strong>no red</strong>.</p> +<div id="scroll"> + <div class="sticky"></div> +</div> + +<script> +window.onload = () => { + document.getElementById("scroll").style.width = "100px"; +}; +</script> diff --git a/tests/wpt/tests/css/css-position/sticky/position-sticky-scrolled-remove-sibling-002.html b/tests/wpt/tests/css/css-position/sticky/position-sticky-scrolled-remove-sibling-002.html new file mode 100644 index 00000000000..870efa417c7 --- /dev/null +++ b/tests/wpt/tests/css/css-position/sticky/position-sticky-scrolled-remove-sibling-002.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com"> +<link rel="author" title="Mozilla" href="https://www.mozilla.org/"> +<link rel="help" href="https://drafts.csswg.org/css-position/#sticky-pos"> +<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1612561"> +<link rel="match" href="../../reference/ref-filled-green-100px-square.xht"> +<meta name="assert" content="This test verifies that there is no vertical scrollbar after removing the sibling that overflows the scroll container"> + +<style> +#scroll { + width: 100px; + height: 100px; + overflow: auto; + background: green; +} +.sticky { + position: sticky; + width: 50px; + height: 100px; + top: 0; + margin-bottom: -100px; +} +#sibling { + width: 50px; + height: 500px; + background: red; +} +</style> + +<p>Test passes if there is a filled green square and <strong>no red</strong>.</p> +<div id="scroll"> + <div class="sticky"></div> + <div id="sibling"></div> +</div> + +<script> +window.onload = () => { + document.getElementById("scroll").scrollTop = 200; + document.getElementById("sibling").remove(); +}; +</script> diff --git a/tests/wpt/tests/css/css-position/sticky/position-sticky-top-004.html b/tests/wpt/tests/css/css-position/sticky/position-sticky-top-004.html new file mode 100644 index 00000000000..bf0b87ab36d --- /dev/null +++ b/tests/wpt/tests/css/css-position/sticky/position-sticky-top-004.html @@ -0,0 +1,37 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>CSS Position Test: Test position:sticky element with 100% top in a block container</title> +<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com"> +<link rel="author" title="Mozilla" href="https://www.mozilla.org/"> +<link rel="help" href="https://drafts.csswg.org/css-position/#sticky-pos"> +<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1748891"> +<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1835705"> +<link rel="match" href="../../reference/ref-filled-green-100px-square.xht"> +<meta name="assert" content="This test verifies that a position:sticky element with 100% top value does not cause the scrollbar to show when shrinking the height of its container."> + +<style> +#scroll { + width: 100px; + height: 200px; + overflow: auto; + background: red; +} +.sticky { + position: sticky; + width: 100px; + height: 100px; + top: 100%; + background: green; +} +</style> + +<p>Test passes if there is a filled green square and <strong>no red</strong>.</p> +<div id="scroll"> + <div class="sticky"></div> +</div> + +<script> +window.onload = () => { + document.getElementById("scroll").style.height = "100px"; +}; +</script> diff --git a/tests/wpt/tests/css/css-position/sticky/position-sticky-top-005.html b/tests/wpt/tests/css/css-position/sticky/position-sticky-top-005.html new file mode 100644 index 00000000000..5f76a722b52 --- /dev/null +++ b/tests/wpt/tests/css/css-position/sticky/position-sticky-top-005.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>CSS Position Test: Test position:sticky element with 100% top in a grid container</title> +<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com"> +<link rel="author" title="Mozilla" href="https://www.mozilla.org/"> +<link rel="help" href="https://drafts.csswg.org/css-position/#sticky-pos"> +<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1748891"> +<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1835705"> +<link rel="match" href="../../reference/ref-filled-green-100px-square.xht"> +<meta name="assert" content="This test verifies that a position:sticky element with 100% top value does not cause the scrollbar to show when shrinking the height of its container."> + +<style> +#scroll { + display: grid; + width: 100px; + height: 200px; + overflow: auto; + background: red; +} +.sticky { + position: sticky; + width: 100px; + height: 100px; + top: 100%; + background: green; +} +</style> + +<p>Test passes if there is a filled green square and <strong>no red</strong>.</p> +<div id="scroll"> + <div class="sticky"></div> +</div> + +<script> +window.onload = () => { + document.getElementById("scroll").style.height = "100px"; +}; +</script> diff --git a/tests/wpt/tests/css/css-position/sticky/position-sticky-top-006.html b/tests/wpt/tests/css/css-position/sticky/position-sticky-top-006.html new file mode 100644 index 00000000000..4ac5310bf35 --- /dev/null +++ b/tests/wpt/tests/css/css-position/sticky/position-sticky-top-006.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>CSS Position Test: Test position:sticky element with 100% top in a flex container</title> +<link rel="author" title="Ting-Yu Lin" href="mailto:tlin@mozilla.com"> +<link rel="author" title="Mozilla" href="https://www.mozilla.org/"> +<link rel="help" href="https://drafts.csswg.org/css-position/#sticky-pos"> +<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1748891"> +<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1835705"> +<link rel="match" href="../../reference/ref-filled-green-100px-square.xht"> +<meta name="assert" content="This test verifies that a position:sticky element with 100% top value does not cause the scrollbar to show when shrinking the height of its container."> + +<style> +#scroll { + display: flex; + width: 100px; + height: 200px; + overflow: auto; + background: red; +} +.sticky { + position: sticky; + width: 100px; + height: 100px; + top: 100%; + background: green; +} +</style> + +<p>Test passes if there is a filled green square and <strong>no red</strong>.</p> +<div id="scroll"> + <div class="sticky"></div> +</div> + +<script> +window.onload = () => { + document.getElementById("scroll").style.height = "100px"; +}; +</script> diff --git a/tests/wpt/tests/css/css-pseudo/highlight-cascade-002-ref.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade-002-ref.html deleted file mode 100644 index 17629a6dbad..00000000000 --- a/tests/wpt/tests/css/css-pseudo/highlight-cascade-002-ref.html +++ /dev/null @@ -1,42 +0,0 @@ -<!doctype html> -<meta charset="utf-8"> -<link rel="author" title="Delan Azabani" href="mailto:dazabani@igalia.com"> -<script src="support/selections.js"></script> -<link rel="stylesheet" href="support/highlights.css"> -<style> - main { - font-size: 7em; - margin: 0.5em; - } - main::selection { - color: black; - background-color: transparent; - } - main > .control > span::selection, - main > .bg > span::selection { - color: white; - background-color: green; - } - main > .fg > span::selection { - color: green; - background-color: white; - } -</style> -<p>Test passes if the words below are (respectively): white on green, green on white, white on green. -<!-- - The element tree below is intentionally the same shape as the - test, despite the fact that we might be able to simplify it. This - is because multiple impls (including Gecko and Blink) split the - background paints accordingly, which can obscure ink overflow in - some of the highlighted text (especially “f”). ---> -<main class="highlight_reftest" - ><span class="control"><span>foo</span></span - > <span class="fg"><span>b</span></span - ><span class="fg"><span>a</span></span - ><span class="fg"><span>r</span></span - > <span class="bg"><span>b</span></span - ><span class="bg"><span>a</span></span - ><span class="bg"><span>z</span></span - ></main> -<script>selectNodeContents(document.querySelector("main"));</script> diff --git a/tests/wpt/tests/css/css-pseudo/highlight-cascade-002.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade-002.html deleted file mode 100644 index c01d3c796e5..00000000000 --- a/tests/wpt/tests/css/css-pseudo/highlight-cascade-002.html +++ /dev/null @@ -1,120 +0,0 @@ -<!doctype html> -<meta charset="utf-8"> -<title>CSS Pseudo-Elements Test: highlight cascade: custom properties are inherited regardless of inherits flag or inheritedness of referencing property</title> -<link rel="author" title="Delan Azabani" href="mailto:dazabani@igalia.com"> -<link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-cascade"> -<link rel="match" href="highlight-cascade-002-ref.html"> -<meta name="assert" value="This test verifies that, given ::selection styles referencing custom properties, their substitution values are inherited from the parent ::selection styles, even if the property is registered with inherits set to false (--inherits-false) or the referencing property is not an inherited property (background-color). All custom properties are treated as inherited when used in highlight styles in any way."> -<script src="support/selections.js"></script> -<link rel="stylesheet" href="support/highlights.css"> -<style> - /* - Register the custom properties, other than --unregistered, - which would be an inherited property [css-variables-1]. - */ - @property --control { - syntax: "<color>"; - initial-value: green; - inherits: false; - } - @property --inherits-false { - syntax: "<color>"; - initial-value: red; - inherits: false; - } - @property --inherits-true { - syntax: "<color>"; - initial-value: red; - inherits: true; - } - - main { - font-size: 7em; - margin: 0.5em; - } - main::selection { - /* - Don’t visibly highlight the spaces between words. - */ - color: black; - background-color: transparent; - } - - /* - Non-highlight control: if this text is white on red (inherit) - rather than white on green (initial), then @property is not - supported well enough to make this test meaningful. - */ - main > .control { - --control: red; - } - main > .control > span { - color: white; - background-color: var(--control); - } - - main > *::selection { - --inherits-false: green; - --inherits-true: green; - --unregistered: green; - } - - /* - Foreground tests: if the foreground of this text is red or - black (initial) rather than green (inherit), then custom - properties are not being inherited in highlight styles. - - color is an inherited property, but that shouldn’t matter. - */ - main > .fg > span::selection { - background-color: white; - } - main > .fg.inherits-false > span::selection { - color: var(--inherits-false); - } - main > .fg.inherits-true > span::selection { - color: var(--inherits-true); - } - main > .fg.unregistered > span::selection { - color: var(--unregistered); - } - - /* - Background tests: if the background of this text is red or - black (initial) rather than green (inherit), then custom - properties are not being inherited in highlight styles. - - background-color is not an inherited property, but that - shouldn’t matter. - */ - main > .bg > span::selection { - color: white; - } - main > .bg.inherits-false > span::selection { - background-color: var(--inherits-false); - } - main > .bg.inherits-true > span::selection { - background-color: var(--inherits-true); - } - main > .bg.unregistered > span::selection { - background-color: var(--unregistered); - } -</style> -<p>Test passes if the words below are (respectively): white on green, green on white, white on green. -<main class="highlight_reftest" - ><span class="control"><span>foo</span></span - > <span class="fg inherits-false"><span>b</span></span - ><span class="fg inherits-true"><span>a</span></span - ><span class="fg unregistered"><span>r</span></span - > <span class="bg inherits-false"><span>b</span></span - ><span class="bg inherits-true"><span>a</span></span - ><span class="bg unregistered"><span>z</span></span - ></main> -<script> - const main = document.querySelector("main"); - selectRangeWith(range => { - range.selectNodeContents(main); - range.setStart(main, 2); - range.setEnd(main, 9); - }); -</script> diff --git a/tests/wpt/tests/css/css-pseudo/cascade-highlight-001-ref.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/cascade-highlight-001-ref.html index 25dbeadb2ee..2a2907b10a4 100644 --- a/tests/wpt/tests/css/css-pseudo/cascade-highlight-001-ref.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/cascade-highlight-001-ref.html @@ -6,7 +6,7 @@ <link rel="author" title="Gérard Talbot" href="http://www.gtalbot.org/BrowserBugsSection/css21testsuite/"> - <link rel="stylesheet" href="support/highlights.css"> + <link rel="stylesheet" href="../support/highlights.css"> <style> div { diff --git a/tests/wpt/tests/css/css-pseudo/cascade-highlight-001.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/cascade-highlight-001.html index 6068a076f0d..f441adf3b90 100644 --- a/tests/wpt/tests/css/css-pseudo/cascade-highlight-001.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/cascade-highlight-001.html @@ -15,7 +15,7 @@ <meta name="assert" content="This test is an adaptation (or modified version) of Example 11 (#example-c35bf49a). The 'div > span::selection' selector has an higher specificity than the 'span::selection' selector."> - <link rel="stylesheet" href="support/highlights.css"> + <link rel="stylesheet" href="../support/highlights.css"> <style> div { diff --git a/tests/wpt/tests/css/css-pseudo/cascade-highlight-002.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/cascade-highlight-002.html index 50be805f011..d10bdb9b042 100644 --- a/tests/wpt/tests/css/css-pseudo/cascade-highlight-002.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/cascade-highlight-002.html @@ -15,7 +15,7 @@ <meta name="assert" content="This test is an adaptation (or modified version) of Example 12 (#example-97480f68). In this test, <span> element's ::selection matches the ::selection { background-color: green; } rule and not the div#test::selection rule because '*' is implied when a tag selector is missing."> - <link rel="stylesheet" href="support/highlights.css"> + <link rel="stylesheet" href="../support/highlights.css"> <style> div { diff --git a/tests/wpt/tests/css/css-pseudo/cascade-highlight-004-ref.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/cascade-highlight-004-ref.html index e755283a32d..9b8507c4cc8 100644 --- a/tests/wpt/tests/css/css-pseudo/cascade-highlight-004-ref.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/cascade-highlight-004-ref.html @@ -6,7 +6,7 @@ <link rel="author" title="Gérard Talbot" href="http://www.gtalbot.org/BrowserBugsSection/css21testsuite/"> - <link rel="stylesheet" href="support/highlights.css"> + <link rel="stylesheet" href="../support/highlights.css"> <style> div { diff --git a/tests/wpt/tests/css/css-pseudo/cascade-highlight-004.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/cascade-highlight-004.html index 56abba71569..295321a1720 100644 --- a/tests/wpt/tests/css/css-pseudo/cascade-highlight-004.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/cascade-highlight-004.html @@ -19,7 +19,7 @@ https://www.w3.org/TR/css-pseudo-4/#highlight-cascade --> - <link rel="stylesheet" href="support/highlights.css"> + <link rel="stylesheet" href="../support/highlights.css"> <style> div { diff --git a/tests/wpt/tests/css/css-pseudo/reference/cascade-highlight-005-ref.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/cascade-highlight-005-ref.html index d1e597e05b8..d1e597e05b8 100644 --- a/tests/wpt/tests/css/css-pseudo/reference/cascade-highlight-005-ref.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/cascade-highlight-005-ref.html diff --git a/tests/wpt/tests/css/css-pseudo/cascade-highlight-005.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/cascade-highlight-005.html index 2c6ba60270c..25ad85fe707 100644 --- a/tests/wpt/tests/css/css-pseudo/cascade-highlight-005.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/cascade-highlight-005.html @@ -6,7 +6,7 @@ <link rel="author" title="Gérard Talbot" href="http://www.gtalbot.org/BrowserBugsSection/css21testsuite/"> <link rel="help" href="https://www.w3.org/TR/css-pseudo-4/#highlight-cascade"> - <link rel="match" href="reference/cascade-highlight-005-ref.html"> + <link rel="match" href="cascade-highlight-005-ref.html"> <meta name="assert" content="In this test, 'background-color' has not been specified a value for the highlight pseudo-element of the span element. Since the span's parent element has an highlight pseudo-element also, then the span's background color for its highlight pseudo-element should be inherited from its parent highlight pseudo-element. Therefore the span element should have a green background color."> @@ -28,7 +28,7 @@ https://www.w3.org/TR/css-pseudo-4/#highlight-cascade --> - <link rel="stylesheet" href="support/highlights.css"> + <link rel="stylesheet" href="../support/highlights.css"> <style> div diff --git a/tests/wpt/tests/css/css-pseudo/highlight-cascade-001-ref.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-001-ref.html index a18690962fc..b96ffce5ab3 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-cascade-001-ref.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-001-ref.html @@ -2,7 +2,7 @@ <meta charset="utf-8"> <link rel="author" title="Delan Azabani" href="mailto:dazabani@igalia.com"> <script src="support/selections.js"></script> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> main { font-size: 7em; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-cascade-001.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-001.html index f237e9eca73..18b3635b3f7 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-cascade-001.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-001.html @@ -6,7 +6,7 @@ <link rel="match" href="highlight-cascade-001-ref.html"> <meta name="assert" value="This test verifies that, given ::selection styles with both color and background-color declared as unset, both properties inherit their values from the parent ::selection styles. All properties become inherited for the purposes of deciding whether unset should mean initial or inherit."> <script src="support/selections.js"></script> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> main { font-size: 7em; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-cascade-003-ref.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-003-ref.html index 256c28ba0c0..68964149baf 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-cascade-003-ref.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-003-ref.html @@ -1,7 +1,7 @@ <!DOCTYPE html> <title>Initial custom property values in :root::selection rule</title> <link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-cascade"> -<script src="support/selections.js"></script> +<script src="../support/selections.js"></script> <style> :root::selection { background-color: green; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-cascade-003.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-003.html index 61bc46d4c50..b29f37528c0 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-cascade-003.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-003.html @@ -3,7 +3,7 @@ <link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-cascade"> <link rel="match" href="highlight-cascade-003-ref.html"> <meta name="assert" value="This test verifies that the initial value given in a custom property registration is respected, when the property is referenced in ::selection styles but no value is defined. The initial value is not the guaranteed-invalid value, so the fallback value in var() is not used."> -<script src="support/selections.js"></script> +<script src="../support/selections.js"></script> <style> @property --bg { syntax: "<color>"; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-cascade-004-ref.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-004-ref.html index f9bf83f9c1d..5845e936209 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-cascade-004-ref.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-004-ref.html @@ -1,7 +1,7 @@ <!DOCTYPE html> <title>Initial custom property values in div::selection rule</title> <link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-cascade"> -<script src="support/selections.js"></script> +<script src="../support/selections.js"></script> <style> div::selection { background-color: green; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-cascade-004.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-004.html index eb181096ce1..f3155bcec6b 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-cascade-004.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-004.html @@ -3,7 +3,7 @@ <link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-cascade"> <link rel="match" href="highlight-cascade-004-ref.html"> <meta name="assert" value="This test verifies that the initial value given in a custom property registration is respected, when the property is referenced in ::selection styles but no value is defined. The initial value is not the guaranteed-invalid value, so the fallback value in var() is not used."> -<script src="support/selections.js"></script> +<script src="../support/selections.js"></script> <style> @property --bg { syntax: "<color>"; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-cascade-005-ref.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-005-ref.html index 20d2b0bdb3a..f0ea3faa764 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-cascade-005-ref.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-005-ref.html @@ -3,7 +3,7 @@ <title>CSS Pseudo-Elements Test: highlight cascade: inheritance with both universal and non-universal rules</title> <link rel="author" title="Delan Azabani" href="mailto:dazabani@igalia.com"> <script src="support/selections.js"></script> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> nav::selection, span::selection, diff --git a/tests/wpt/tests/css/css-pseudo/highlight-cascade-005.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-005.html index 008f56aded9..958bdf65447 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-cascade-005.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-005.html @@ -6,7 +6,7 @@ <link rel="match" href="highlight-cascade-005-ref.html"> <meta name="assert" content="This test verifies that, given both universal and non-universal ::selection rules, the subject of the non-universal rule has styles from both rules with the non-universal styles overriding the universal styles (due to the cascade), its descendants have styles from both rules with the universal styles overriding the non-universal styles (due to highlight inheritance), and its siblings have the universal styles only."> <script src="support/selections.js"></script> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> /* * (universal) */::selection { background-color: green; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-cascade-006-ref.xhtml b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-006-ref.xhtml index df0a56123e0..643dd793254 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-cascade-006-ref.xhtml +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-006-ref.xhtml @@ -2,7 +2,7 @@ <meta charset="utf-8"/> <title>CSS Pseudo-Elements Test: highlight cascade: inheritance with both universal and namespace-universal rules</title> <link rel="author" title="Delan Azabani" href="mailto:dazabani@igalia.com"/> -<script src="support/selections.js"></script> +<script src="../support/selections.js"></script> <style> main * { all: initial; display: block; } ::selection { color: green; } diff --git a/tests/wpt/tests/css/css-pseudo/highlight-cascade-006.xhtml b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-006.xhtml index fb6d07f8f0b..4a37af7c255 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-cascade-006.xhtml +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-006.xhtml @@ -5,7 +5,7 @@ <link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-cascade"/> <link rel="match" href="highlight-cascade-006-ref.xhtml"/> <meta name="assert" content="This test verifies that, given both universal ::selection rules and ::selection rules that are actually non-universal due to an explicit namespace prefix or default @namespace rule, the non-universal rules are not erroneously treated as universal."/> -<script src="support/selections.js"></script> +<script src="../support/selections.js"></script> <style> main * { all: initial; display: block; } ::selection { color: green; } /* 1. universal (* means *|* if there is no default @namespace) */ diff --git a/tests/wpt/tests/css/css-pseudo/highlight-cascade-007.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-007.html index de0322d9103..df79d8dff51 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-cascade-007.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-007.html @@ -5,8 +5,8 @@ <link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-cascade"> <link rel="help" href="https://github.com/w3c/csswg-drafts/issues/7591"> <meta name="assert" content="This test verifies that non-applicable property declarations are ignored in highlight pseudos, that the computed values of ‘font-size’ and ‘line-height’ in highlight pseudos are taken from the originating element, and that ‘text-shadow’ in highlight pseudos respects these values when given ‘em’ and ‘lh’ units."> -<script src="support/selections.js"></script> -<link rel="stylesheet" href="support/highlights.css"> +<script src="../support/selections.js"></script> +<link rel="stylesheet" href="../support/highlights.css"> <script src="/resources/testharness.js"></script> <script src="/resources/testharnessreport.js"></script> <style> diff --git a/tests/wpt/tests/css/css-pseudo/highlight-cascade-008-ref.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-008-ref.html index c87ddcf93c9..70b9d958b5f 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-cascade-008-ref.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-008-ref.html @@ -1,6 +1,6 @@ <!DOCTYPE html> <title>Custom property values from the root element</title> -<script src="support/selections.js"></script> +<script src="../support/selections.js"></script> <style> div::selection { background-color: green; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-cascade-008.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-008.html index 10ca924b8fd..720e2f0469d 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-cascade-008.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-008.html @@ -1,18 +1,19 @@ <!DOCTYPE html> -<title>Custom property values from the root element</title> +<title>Custom property values from the originating element</title> <link rel="author" title="Stephen Chenney" href="mailto:schenney@chromium.org"> <link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-cascade"> <link rel="match" href="highlight-cascade-008-ref.html"> -<meta name="assert" value="This test verifies that when a custom property is not found in highlight cascade, its value is taken from the root element."> +<meta name="assert" value="This test verifies that custom properties from the originating element are used to resolve var() on highlight pseudos."> <meta name="fuzzy" content="0-255;0-10"> -<script src="support/selections.js"></script> +<script src="../support/selections.js"></script> <style> :root { --background-color: green; - --decoration-color: purple; + --decoration-color: yellow; } ::selection { - --decoration-color: yellow; + --background-color: cyan; + --decoration-color: magenta; } div::selection { background-color: var(--background-color, red); @@ -20,8 +21,11 @@ text-decoration-style: line; text-decoration-color: var(--decoration-color, red); } - span::selection { + span { --background-color: blue; + } + span::selection { + --background-color: purple; background-color: var(--background-color, red); } </style> diff --git a/tests/wpt/tests/css/css-pseudo/highlight-cascade-009.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-009.html index acc48c7c364..e95de9a7815 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-cascade-009.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-cascade-009.html @@ -4,18 +4,18 @@ <link rel="author" title="Stephen Chenney" href="mailto:schenney@chromium.org"> <link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-cascade"> <link rel="help" href="https://github.com/w3c/csswg-drafts/issues/6641"> -<meta name="assert" content="This test verifies that custom properties used in highlight pseudos are taken from the ::root if not found in the highlight inheritance chain."> -<script src="support/selections.js"></script> -<link rel="stylesheet" href="support/highlights.css"> +<meta name="assert" content="This test verifies that custom properties used in highlight pseudos are taken from the originating element."> +<script src="../support/selections.js"></script> +<link rel="stylesheet" href="../support/highlights.css"> <script src="/resources/testharness.js"></script> <script src="/resources/testharnessreport.js"></script> <style> :root { --background-color: green; - --decoration-color: purple; + --decoration-color: green; } ::selection { - --decoration-color: green; + --decoration-color: purple; } div::selection { --background-color: blue; @@ -31,11 +31,11 @@ const body_selection = getComputedStyle(document.querySelector("body"), "::selection"); const div_selection = getComputedStyle(document.querySelector("div"), "::selection"); test(() => void assert_equals(body_selection.getPropertyValue("--background-color"), "green"), - "body ::selection has the root custom property"); + "body ::selection uses the originating custom property"); test(() => void assert_equals(body_selection.getPropertyValue("--decoration-color"), "green"), - "body ::selection uses its own custom property"); + "body ::selection does not use its own custom property"); test(() => void assert_equals(div_selection.getPropertyValue("--decoration-color"), "green"), - "div::selection inherits a custom property"); - test(() => void assert_equals(div_selection.getPropertyValue("--background-color"), "blue"), - "div::selection uses its own custom property"); + "div::selection uses the originating element custom property"); + test(() => void assert_equals(div_selection.getPropertyValue("--background-color"), "green"), + "div::selection does not use its own custom property"); </script> diff --git a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-computed-inheritance.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-computed-inheritance.html index d67ae828818..d67ae828818 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-computed-inheritance.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-computed-inheritance.html diff --git a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-computed-visited.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-computed-visited.html index 207cb7b7dd1..207cb7b7dd1 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-computed-visited.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-computed-visited.html diff --git a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-computed.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-computed.html index 97c31809dfc..97c31809dfc 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-computed.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-computed.html diff --git a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-painting-properties-001-ref.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-001-ref.html index 93e2002f4aa..d3a3d49797a 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-painting-properties-001-ref.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-001-ref.html @@ -3,7 +3,7 @@ <meta charset="utf-8" /> <title>CSS Pseudo-Elements Test: Reference</title> <link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com"> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> div { color: lime; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-painting-properties-001.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-001.html index 76d80d228a0..efdb9016bc2 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-painting-properties-001.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-001.html @@ -5,7 +5,7 @@ <link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com"> <link rel="help" href="https://drafts.csswg.org/css-pseudo/#highlight-text"> <link rel="match" href="highlight-currentcolor-painting-properties-001-ref.html"> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <meta name="fuzzy" content="0-50;0-150"> <style> div { diff --git a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-painting-properties-002-ref.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-002-ref.html index 11ca501065e..6f9066cba8a 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-painting-properties-002-ref.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-002-ref.html @@ -3,7 +3,7 @@ <meta charset="utf-8" /> <title>CSS Pseudo-Elements Test: Reference</title> <link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com"> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> div { color: lime; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-painting-properties-002.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-002.html index ac3677c1cff..d02dc9e8c5b 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-painting-properties-002.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-properties-002.html @@ -5,7 +5,7 @@ <link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com"> <link rel="help" href="https://drafts.csswg.org/css-pseudo/#highlight-text"> <link rel="match" href="highlight-currentcolor-painting-properties-002-ref.html"> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <meta name="fuzzy" content="0-50;0-150"> <style> div { diff --git a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-painting-text-shadow-001-ref.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-001-ref.html index d6ee33cda74..1ab60db5c8e 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-painting-text-shadow-001-ref.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-001-ref.html @@ -1,5 +1,5 @@ <!DOCTYPE html><meta charset="utf-8"> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> div { color: lime; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-painting-text-shadow-001.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-001.html index 141556f935b..c82da8c380e 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-painting-text-shadow-001.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-001.html @@ -4,7 +4,7 @@ <link rel="author" title="Delan Azabani" href="mailto:dazabani@igalia.com"> <link rel="help" href="https://drafts.csswg.org/css-pseudo/#highlight-text"> <link rel="match" href="highlight-currentcolor-painting-text-shadow-001-ref.html"> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> div { color: lime; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-painting-text-shadow-002-ref.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-002-ref.html index fb2696a55a1..6452a34d0e7 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-painting-text-shadow-002-ref.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-002-ref.html @@ -1,5 +1,5 @@ <!DOCTYPE html><meta charset="utf-8"> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> div { color: lime; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-painting-text-shadow-002.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-002.html index 77858729afa..870243f5013 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-painting-text-shadow-002.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-painting-text-shadow-002.html @@ -4,7 +4,7 @@ <link rel="author" title="Delan Azabani" href="mailto:dazabani@igalia.com"> <link rel="help" href="https://drafts.csswg.org/css-pseudo/#highlight-text"> <link rel="match" href="highlight-currentcolor-painting-text-shadow-002-ref.html"> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> div { color: lime; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-root-explicit-default-001-ref.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-001-ref.html index 794796a88f6..70ce6b59a49 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-root-explicit-default-001-ref.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-001-ref.html @@ -3,7 +3,7 @@ <meta charset="utf-8" /> <title>CSS Pseudo-Elements Test: Reference</title> <link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com"> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> div { color: lime; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-root-explicit-default-001.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-001.html index a1512f014df..1869f8ab53f 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-root-explicit-default-001.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-001.html @@ -5,7 +5,7 @@ <link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com"> <link rel="help" href="https://drafts.csswg.org/css-pseudo/#highlight-text"> <link rel="match" href="highlight-currentcolor-root-explicit-default-001-ref.html"> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> div { color: lime; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-root-explicit-default-002-ref.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-002-ref.html index 31759483174..b64b007c64b 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-root-explicit-default-002-ref.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-002-ref.html @@ -3,7 +3,7 @@ <meta charset="utf-8" /> <title>CSS Pseudo-Elements Test: Reference</title> <link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com"> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> .highlight_reftest { color: green; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-root-explicit-default-002.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-002.html index fc5698faa1e..fe6d80be2e9 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-root-explicit-default-002.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-explicit-default-002.html @@ -5,7 +5,7 @@ <link rel="author" title="Manuel Rego Casasnovas" href="mailto:rego@igalia.com"> <link rel="help" href="https://drafts.csswg.org/css-pseudo/#highlight-text"> <link rel="match" href="highlight-currentcolor-root-explicit-default-002-ref.html"> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> .highlight_reftest { color: green; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-root-implicit-default-001.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-implicit-default-001.html index ecf787b9b41..ecf787b9b41 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-root-implicit-default-001.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-implicit-default-001.html diff --git a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-root-implicit-default-002.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-implicit-default-002.html index 420cc5ba15e..420cc5ba15e 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-root-implicit-default-002.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-implicit-default-002.html diff --git a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-root-implicit-default-ref.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-implicit-default-ref.html index 67ecb8df640..67ecb8df640 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-currentcolor-root-implicit-default-ref.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-currentcolor-root-implicit-default-ref.html diff --git a/tests/wpt/tests/css/css-pseudo/highlight-paired-cascade-001-ref.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-001-ref.html index 14687acb841..102b07b4814 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-paired-cascade-001-ref.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-001-ref.html @@ -2,7 +2,7 @@ <meta charset="utf-8"> <link rel="author" name="Delan Azabani" href="mailto:dazabani@igalia.com"> <script src="support/selections.js"></script> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> main { font-size: 7em; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-paired-cascade-001.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-001.html index 09e5abf9a32..7f370238dfa 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-paired-cascade-001.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-001.html @@ -6,7 +6,7 @@ <link rel="match" href="highlight-paired-cascade-001-ref.html"> <meta name="assert" value="This test verifies that setting color on ::selection suppresses any UA non-initial used value for background-color. These properties are highlight colors, which are subject to paired cascade."> <script src="support/selections.js"></script> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> main { font-size: 7em; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-paired-cascade-002-ref.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-002-ref.html index 48eb9911a10..19d731af02c 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-paired-cascade-002-ref.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-002-ref.html @@ -2,7 +2,7 @@ <meta charset="utf-8"> <link rel="author" name="Delan Azabani" href="mailto:dazabani@igalia.com"> <script src="support/selections.js"></script> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> main { font-size: 7em; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-paired-cascade-002.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-002.html index affbe956297..626fc57558d 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-paired-cascade-002.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-002.html @@ -6,7 +6,7 @@ <link rel="match" href="highlight-paired-cascade-002-ref.html"> <meta name="assert" value="This test verifies that setting background-color on ::selection suppresses any UA non-initial used value for color. These properties are highlight colors, which are subject to paired cascade."> <script src="support/selections.js"></script> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> main { font-size: 7em; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-paired-cascade-003-ref.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-003-ref.html index 18885fdc898..638ae00c685 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-paired-cascade-003-ref.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-003-ref.html @@ -2,7 +2,7 @@ <meta charset="utf-8"> <link rel="author" name="Delan Azabani" href="mailto:dazabani@igalia.com"> <script src="support/selections.js"></script> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> main { font-size: 7em; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-paired-cascade-003.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-003.html index 250e320210c..8c621cc777c 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-paired-cascade-003.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-003.html @@ -6,7 +6,7 @@ <link rel="match" href="highlight-paired-cascade-003-ref.html"> <meta name="assert" value="This test verifies that setting text-decoration on ::selection does not suppress any UA non-initial used values for color or background-color. While the former is an applicable (shorthand) property for highlight styles, it is not one of the highlight colors (color or background-color), so paired cascade does not apply."> <script src="support/selections.js"></script> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> main { font-size: 7em; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-paired-cascade-004-notref.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-004-notref.html index 63472b67589..72490b1461d 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-paired-cascade-004-notref.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-004-notref.html @@ -1,7 +1,7 @@ <!doctype html> <meta charset="utf-8"> <link rel="author" name="Delan Azabani" href="mailto:dazabani@igalia.com"> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> :link, :visited { color: blue; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-paired-cascade-004.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-004.html index 61e2b7d7f07..0a73d006b55 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-paired-cascade-004.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-004.html @@ -5,7 +5,7 @@ <link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-cascade"> <link rel="mismatch" href="highlight-paired-cascade-004-notref.html"> <meta name="assert" value="This test verifies that setting color on ::target-text suppresses any UA non-initial used value for background-color. ::target-text is a highlight pseudo with a recommended UA default background-color that is not initial (Mark), so paired cascade can be observed."> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> :link, :visited { color: blue; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-paired-cascade-005-ref.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-005-ref.html index 0ac5c02b203..862d06e6f64 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-paired-cascade-005-ref.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-005-ref.html @@ -2,7 +2,7 @@ <meta charset="utf-8"> <link rel="author" name="Delan Azabani" href="mailto:dazabani@igalia.com"> <script src="support/selections.js"></script> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> main { font-size: 7em; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-paired-cascade-005.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-005.html index 50677c811e7..4ee6db3a091 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-paired-cascade-005.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-005.html @@ -6,7 +6,7 @@ <link rel="match" href="highlight-paired-cascade-005-ref.html"> <meta name="assert" value="This test verifies that setting color to unset on ::selection suppresses any UA non-initial used value for background-color. The unset value is defined as both “treated as [inherit or initial depending on whether the property is inherited]” and “effectively erases all declared values occurring earlier in the cascade [(inclusive)]”, which are normally equivalent, but under paired cascade, the former wins."> <script src="support/selections.js"></script> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> main { font-size: 7em; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-paired-cascade-006-ref.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-006-ref.html index 18885fdc898..638ae00c685 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-paired-cascade-006-ref.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-006-ref.html @@ -2,7 +2,7 @@ <meta charset="utf-8"> <link rel="author" name="Delan Azabani" href="mailto:dazabani@igalia.com"> <script src="support/selections.js"></script> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> main { font-size: 7em; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-paired-cascade-006.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-006.html index 20c03282c81..bef3601ccbf 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-paired-cascade-006.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-paired-cascade-006.html @@ -6,7 +6,7 @@ <link rel="match" href="highlight-paired-cascade-006-ref.html"> <meta name="assert" value="This test verifies that setting color to revert on ::selection does not suppress any UA non-initial used value for background-color. Because the revert value rolls back the cascade, it destroys its own existence as a cascaded value, and this is also true under paired cascade."> <script src="support/selections.js"></script> -<link rel="stylesheet" href="support/highlights.css"> +<link rel="stylesheet" href="../support/highlights.css"> <style> main { font-size: 7em; diff --git a/tests/wpt/tests/css/css-pseudo/highlight-pseudos-computed.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-pseudos-computed.html index 4a274e1bbd8..4a274e1bbd8 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-pseudos-computed.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-pseudos-computed.html diff --git a/tests/wpt/tests/css/css-pseudo/highlight-pseudos-inheritance-computed-001.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-pseudos-inheritance-computed-001.html index 84c4045a54c..84c4045a54c 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-pseudos-inheritance-computed-001.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-pseudos-inheritance-computed-001.html diff --git a/tests/wpt/tests/css/css-pseudo/highlight-pseudos-visited-computed-001.html b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-pseudos-visited-computed-001.html index a2b18effccd..a2b18effccd 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-pseudos-visited-computed-001.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-cascade/highlight-pseudos-visited-computed-001.html diff --git a/tests/wpt/tests/css/css-pseudo/highlight-custom-properties-dynamic-001-ref.html b/tests/wpt/tests/css/css-pseudo/highlight-custom-properties-dynamic-001-ref.html new file mode 100644 index 00000000000..836874df056 --- /dev/null +++ b/tests/wpt/tests/css/css-pseudo/highlight-custom-properties-dynamic-001-ref.html @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<meta charset="UTF-8"> +<title>CSS Pseudo Test: Dynamic Custom Properties for Highlights</title> +<link rel="author" title="Stephen Chenney" href="mailto:schenney@igalia.com"> +<head> + <style> + ::selection { + background-color: green; + } + </style> +</head> +<body>Green background when selected</body> +<script> + window.getSelection().selectAllChildren(document.body); +</script> +</html>
\ No newline at end of file diff --git a/tests/wpt/tests/css/css-pseudo/highlight-custom-properties-dynamic-001.html b/tests/wpt/tests/css/css-pseudo/highlight-custom-properties-dynamic-001.html new file mode 100644 index 00000000000..836ef546fa6 --- /dev/null +++ b/tests/wpt/tests/css/css-pseudo/highlight-custom-properties-dynamic-001.html @@ -0,0 +1,28 @@ +<!DOCTYPE html> +<meta charset="UTF-8"> +<title>CSS Pseudo Test: Dynamic Custom Properties for Highlights</title> +<link rel="help" href="https://drafts.csswg.org/css-pseudo/#highlight-styling"> +<link rel="match" href="highlight-custom-properties-dynamic-001-ref.html"> +<link rel="author" title="Stephen Chenney" href="mailto:schenney@igalia.com"> +<meta name="assert" value="Custom property values in highlights update correctly when the property is updated."> +<head> + <style> + div { + --background-color: red; + } + ::selection { + background-color: var(--background-color, red); + } + </style> +</head> +<div id="originating">Green background when selected</div> +<script> + window.getSelection().selectAllChildren(document.body); + requestAnimationFrame(() => { + requestAnimationFrame(() => { + originating.style.setProperty("--background-color", "green"); + requestAnimationFrame(() => takeScreenshot()); + }); + }); +</script> +</html>
\ No newline at end of file diff --git a/tests/wpt/tests/css/css-pseudo/highlight-painting-shadows-horizontal-ref.html b/tests/wpt/tests/css/css-pseudo/highlight-painting-shadows-horizontal-ref.html new file mode 100644 index 00000000000..4ff90afb388 --- /dev/null +++ b/tests/wpt/tests/css/css-pseudo/highlight-painting-shadows-horizontal-ref.html @@ -0,0 +1,73 @@ +<!doctype html> +<meta charset="utf-8"> +<title>CSS Pseudo-Elements Test: Shadows on highlights horizontal text - reference</title> +<link rel="author" name="Stephen Chenney" href="mailto:schenney@igalia.com"> +<style> + :root { + line-height: 1; + white-space: break-spaces; + } + #originating_shadow { + font-size: 2em; + color: transparent; + text-shadow: 0.1em 0.1em rgba(0,0,0,0.5); + position: absolute; + z-index: 0; + top: 10px; + left: 10px; + } + #originating_text { + font-size: 2em; + color: black; + position: absolute; + z-index: 7; + top: 10px; + left: 10px; + } + #selection_only { + font-size: 2em; + color: green; + text-shadow: -0.25em -0.25em rgba(0,128,0,0.5), 0.1em 0.1em rgba(0,0,0,0.5); + position: absolute; + z-index: 5; + top: 10px; + } + #target { + font-size: 2em; + color: blue; + text-shadow: 0.25em 0.25em rgba(0,0,128,0.5), 0.1em 0.1em rgba(0,0,0,0.5); + position: absolute; + z-index: 3; + top: 10px; + } + #both_text { + font-size: 2em; + color: green; + position: absolute; + z-index: 4; + top: 10px; + } + #both_shadow { + font-size: 2em; + color: transparent; + text-shadow: 0.25em 0.25em rgba(0,0,128,0.5), -0.25em -0.25em rgba(0,128,0,0.5), 0.1em 0.1em rgba(0,0,0,0.5); + position: absolute; + z-index: 2; + top: 10px; + } +</style> +<p id="originating_shadow">part</p> +<p id="originating_text">part</p> +<p id="selection_only">ially selected </p> +<p id="both_text">ta</p> +<p id="both_shadow">ta</p> +<p id="target">rget</p> +<script> + originatingCS = getComputedStyle(originating_text); + selection_only.style.left = (parseFloat(originatingCS.width) + parseFloat(originatingCS.left)).toString() + "px"; + selectionCS = getComputedStyle(selection_only); + both_text.style.left = (parseFloat(selectionCS.width) + parseFloat(selectionCS.left)).toString() + "px"; + both_shadow.style.left = both_text.style.left; + bothCS = getComputedStyle(both_text); + target.style.left = (parseFloat(bothCS.width) + parseFloat(bothCS.left)).toString() + "px"; +</script> diff --git a/tests/wpt/tests/css/css-pseudo/highlight-painting-shadows-horizontal.html b/tests/wpt/tests/css/css-pseudo/highlight-painting-shadows-horizontal.html new file mode 100644 index 00000000000..b1762e85ae3 --- /dev/null +++ b/tests/wpt/tests/css/css-pseudo/highlight-painting-shadows-horizontal.html @@ -0,0 +1,41 @@ +<!doctype html> +<meta charset="utf-8"> +<title>CSS Pseudo-Elements Test: Shadows on highlights horizontal text</title> +<link rel="author" name="Stephen Chenney" href="mailto:schenney@igalia.com"> +<link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-painting"> +<link rel="match" href="highlight-painting-shadows-horizontal-ref.html"> +<meta name="assert" value="::selection and ::target-text both with shadows are painted in the correct order, including originating element shadows"> +<meta name="fuzzy" content="0-32;0-20"> +<script src="support/selections.js"></script> +<style> + :root { + line-height: 1; + white-space: pre; + } + p { + font-size: 2em; + color: black; + text-shadow: 0.1em 0.1em rgba(0,0,0,0.5); + position: absolute; + top: 10px; + left: 10px; + } + p::selection { + color: green; + text-shadow: -0.25em -0.25em rgba(0,128,0,0.5); + } + p::target-text { + color: blue; + text-shadow: 0.25em 0.25em rgba(0,0,128,0.5); + } +</style> +<p>partially selected target</p> +<script> + window.location.hash = "#:~:text=target"; + const target = document.querySelector("p"); + selectRangeWith(range => { + range.selectNodeContents(target); + range.setStart(target.childNodes[0], 4); + range.setEnd(target.childNodes[0], 21); + }); +</script> diff --git a/tests/wpt/tests/css/css-pseudo/highlight-painting-shadows-vertical-ref.html b/tests/wpt/tests/css/css-pseudo/highlight-painting-shadows-vertical-ref.html new file mode 100644 index 00000000000..00a6f1c8088 --- /dev/null +++ b/tests/wpt/tests/css/css-pseudo/highlight-painting-shadows-vertical-ref.html @@ -0,0 +1,70 @@ +<!doctype html> +<meta charset="utf-8"> +<title>CSS Pseudo-Elements Test: Shadows on highlights vertical text - reference</title> +<link rel="author" name="Stephen Chenney" href="mailto:schenney@igalia.com"> +<link rel="stylesheet" type="text/css" href="/fonts/ahem.css"> +<style> + :root { + font-family: Ahem; + writing-mode: vertical-lr; + line-height: 1; + white-space: break-spaces; + } + #originating_shadow { + color: transparent; + text-shadow: 0.1em 0.1em rgba(0,0,0,0.5); + position: absolute; + z-index: 0; + top: 10px; + left: 10px; + } + #originating_text { + color: black; + position: absolute; + z-index: 7; + top: 10px; + left: 10px; + } + #selection_only { + color: green; + text-shadow: -0.25em -0.25em rgba(0,128,0,0.5), 0.1em 0.1em rgba(0,0,0,0.5); + position: absolute; + z-index: 5; + left: 10px; + } + #target { + color: blue; + text-shadow: 0.25em 0.25em rgba(0,0,128,0.5), 0.1em 0.1em rgba(0,0,0,0.5); + position: absolute; + z-index: 3; + left: 10px; + } + #both_text { + color: green; + position: absolute; + z-index: 4; + left: 10px; + } + #both_shadow { + color: transparent; + text-shadow: 0.25em 0.25em rgba(0,0,128,0.5), -0.25em -0.25em rgba(0,128,0,0.5), 0.1em 0.1em rgba(0,0,0,0.5); + position: absolute; + z-index: 2; + left: 10px; + } +</style> +<p id="originating_shadow">part</p> +<p id="originating_text">part</p> +<p id="selection_only">ially selected </p> +<p id="both_text">ta</p> +<p id="both_shadow">ta</p> +<p id="target">rget</p> +<script> + originatingCS = getComputedStyle(originating_text); + selection_only.style.top = (parseFloat(originatingCS.height) + parseFloat(originatingCS.top)).toString() + "px"; + selectionCS = getComputedStyle(selection_only); + both_text.style.top = (parseFloat(selectionCS.height) + parseFloat(selectionCS.top)).toString() + "px"; + both_shadow.style.top = both_text.style.top; + bothCS = getComputedStyle(both_text); + target.style.top = (parseFloat(bothCS.height) + parseFloat(bothCS.top)).toString() + "px"; +</script> diff --git a/tests/wpt/tests/css/css-pseudo/highlight-painting-shadows-vertical.html b/tests/wpt/tests/css/css-pseudo/highlight-painting-shadows-vertical.html new file mode 100644 index 00000000000..7187c34da44 --- /dev/null +++ b/tests/wpt/tests/css/css-pseudo/highlight-painting-shadows-vertical.html @@ -0,0 +1,43 @@ +<!doctype html> +<meta charset="utf-8"> +<title>CSS Pseudo-Elements Test: Shadows on highlights vertical text</title> +<link rel="author" name="Stephen Chenney" href="mailto:schenney@igalia.com"> +<link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-painting"> +<link rel="match" href="highlight-painting-shadows-vertical-ref.html"> +<meta name="assert" value="::selection and ::target-text both with shadows are painted in the correct order, including originating element shadows"> +<meta name="fuzzy" content="0-32;0-4"> +<link rel="stylesheet" type="text/css" href="/fonts/ahem.css"> +<script src="support/selections.js"></script> +<style> + :root { + font-family: Ahem; + writing-mode: vertical-lr; + line-height: 1; + white-space: pre; + } + p { + color: black; + text-shadow: 0.1em 0.1em rgba(0,0,0,0.5); + position: absolute; + top: 10px; + left: 10px; + } + p::selection { + color: green; + text-shadow: -0.25em -0.25em rgba(0,128,0,0.5); + } + p::target-text { + color: blue; + text-shadow: 0.25em 0.25em rgba(0,0,128,0.5); + } +</style> +<p>partially selected target</p> +<script> + window.location.hash = "#:~:text=target"; + const target = document.querySelector("p"); + selectRangeWith(range => { + range.selectNodeContents(target); + range.setStart(target.childNodes[0], 4); + range.setEnd(target.childNodes[0], 21); + }); +</script> diff --git a/tests/wpt/tests/css/css-pseudo/highlight-styling-001.html b/tests/wpt/tests/css/css-pseudo/highlight-styling-001.html index 63d8ee1eda4..7fe76d19380 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-styling-001.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-styling-001.html @@ -1,10 +1,10 @@ <!doctype html> <meta charset="utf-8"> -<title>CSS Pseudo-Elements Test: highlight styling: custom properties are applicable properties in highlight pseudos</title> +<title>CSS Pseudo-Elements Test: highlight styling: custom properties are not applicable properties in highlight pseudos</title> <link rel="author" title="Delan Azabani" href="mailto:dazabani@igalia.com"> <link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-styling"> <link rel="match" href="highlight-styling-001-ref.html"> -<meta name="assert" value="This test verifies that ::selection styles can set and reference custom properties."> +<meta name="assert" value="This test verifies that ::selection styles cannot set custom properties."> <script src="support/selections.js"></script> <link rel="stylesheet" href="support/highlights.css"> <style> @@ -13,9 +13,9 @@ margin: 0.5em; } main::selection { - --x: green; + --x: red; color: white; - background-color: var(--x); + background-color: var(--x, green); } </style> <p>Test passes if the text below is white on green. diff --git a/tests/wpt/tests/css/css-pseudo/highlight-styling-002.html b/tests/wpt/tests/css/css-pseudo/highlight-styling-002.html index 2f7cc29128e..351eacac885 100644 --- a/tests/wpt/tests/css/css-pseudo/highlight-styling-002.html +++ b/tests/wpt/tests/css/css-pseudo/highlight-styling-002.html @@ -1,21 +1,21 @@ <!doctype html> <meta charset="utf-8"> -<title>CSS Pseudo-Elements Test: highlight styling: originating custom property values do not affect highlight pseudos</title> +<title>CSS Pseudo-Elements Test: highlight styling: originating custom property values are used for highlight pseudos</title> <link rel="author" title="Delan Azabani" href="mailto:dazabani@igalia.com"> <link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-styling"> <link rel="match" href="highlight-styling-002-ref.html"> -<meta name="assert" value="This test verifies that custom property values set in originating elements do not participate in the substitution of those properties in ::selection styles."> +<meta name="assert" value="This test verifies that custom property values on the originating element are used for resolving var() in a selection pseudo."> <script src="support/selections.js"></script> <link rel="stylesheet" href="support/highlights.css"> <style> main { font-size: 7em; margin: 0.5em; - --x: red; + --x: green; } main::selection { color: white; - background-color: var(--x, green); + background-color: var(--x, red); } </style> <p>Test passes if the text below is white on green. diff --git a/tests/wpt/tests/css/css-pseudo/target-text-dynamic-004.html b/tests/wpt/tests/css/css-pseudo/target-text-dynamic-004.html index 279ec674b40..35f2542c766 100644 --- a/tests/wpt/tests/css/css-pseudo/target-text-dynamic-004.html +++ b/tests/wpt/tests/css/css-pseudo/target-text-dynamic-004.html @@ -18,7 +18,7 @@ </style> <p>The test passes if the following word has a magenta background.</p> -<div>Example</div> +<div><span>Example</span></div> <script> location.href = "#:~:text=Example"; diff --git a/tests/wpt/tests/css/css-pseudo/target-text-shadow-horizontal-ref.html b/tests/wpt/tests/css/css-pseudo/target-text-shadow-horizontal-ref.html new file mode 100644 index 00000000000..6189f844b5e --- /dev/null +++ b/tests/wpt/tests/css/css-pseudo/target-text-shadow-horizontal-ref.html @@ -0,0 +1,19 @@ +<!doctype html> +<meta charset="utf-8"> +<title>CSS Pseudo-Elements Test: Shadows on horizontal target text - reference</title> +<link rel="author" name="Stephen Chenney" href="mailto:schenney@igalia.com"> +<style> + :root { + line-height: 1; + } + p { + font-size: 2em; + color: black; + text-shadow: 0.1em 0.1em 3px rgba(0,0,0,0.5); + } + span { + color: green; + text-shadow: 0.25em 0.25em 3px rgba(0,0,128,0.5), 0.1em 0.1em 3px rgba(0,0,0,0.5); + } +</style> +<p>the <span>target</span> should have a shadow</p> diff --git a/tests/wpt/tests/css/css-pseudo/target-text-shadow-horizontal.html b/tests/wpt/tests/css/css-pseudo/target-text-shadow-horizontal.html new file mode 100644 index 00000000000..1ded1360ff0 --- /dev/null +++ b/tests/wpt/tests/css/css-pseudo/target-text-shadow-horizontal.html @@ -0,0 +1,25 @@ +<!doctype html> +<meta charset="utf-8"> +<title>CSS Pseudo-Elements Test: Shadows on horizontal target text</title> +<link rel="author" name="Stephen Chenney" href="mailto:schenney@igalia.com"> +<link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-painting"> +<link rel="match" href="target-text-shadow-horizontal-ref.html"> +<meta name="assert" value="::target-text with a shadow is painted, including originating element shadows"> +<style> + :root { + line-height: 1; + } + p { + font-size: 2em; + color: black; + text-shadow: 0.1em 0.1em 3px rgba(0,0,0,0.5); + } + p::target-text { + color: green; + text-shadow: 0.25em 0.25em 3px rgba(0,0,128,0.5); + } +</style> +<p>the target should have a shadow</p> +<script> + window.location.hash = "#:~:text=target"; +</script> diff --git a/tests/wpt/tests/css/css-pseudo/target-text-shadow-vertical-ref.html b/tests/wpt/tests/css/css-pseudo/target-text-shadow-vertical-ref.html new file mode 100644 index 00000000000..cd9e1790537 --- /dev/null +++ b/tests/wpt/tests/css/css-pseudo/target-text-shadow-vertical-ref.html @@ -0,0 +1,20 @@ +<!doctype html> +<meta charset="utf-8"> +<title>CSS Pseudo-Elements Test: Shadows on vertical target text - reference</title> +<link rel="author" name="Stephen Chenney" href="mailto:schenney@igalia.com"> +<style> + :root { + line-height: 1; + writing-mode: vertical-lr; + } + p { + font-size: 2em; + color: black; + text-shadow: 0.1em 0.1em 3px rgba(0,0,0,0.5); + } + span { + color: green; + text-shadow: 0.25em 0.25em 3px rgba(0,0,128,0.5), 0.1em 0.1em 3px rgba(0,0,0,0.5); + } +</style> +<p>the <span>target</span> should have a shadow</p> diff --git a/tests/wpt/tests/css/css-pseudo/target-text-shadow-vertical.html b/tests/wpt/tests/css/css-pseudo/target-text-shadow-vertical.html new file mode 100644 index 00000000000..088f6fc175e --- /dev/null +++ b/tests/wpt/tests/css/css-pseudo/target-text-shadow-vertical.html @@ -0,0 +1,26 @@ +<!doctype html> +<meta charset="utf-8"> +<title>CSS Pseudo-Elements Test: Shadows on vertical target text</title> +<link rel="author" name="Stephen Chenney" href="mailto:schenney@igalia.com"> +<link rel="help" href="https://drafts.csswg.org/css-pseudo-4/#highlight-painting"> +<link rel="match" href="target-text-shadow-vertical-ref.html"> +<meta name="assert" value="::target-text with a shadow is painted, including originating element shadows"> +<style> + :root { + line-height: 1; + writing-mode: vertical-lr; + } + p { + font-size: 2em; + color: black; + text-shadow: 0.1em 0.1em 3px rgba(0,0,0,0.5); + } + p::target-text { + color: green; + text-shadow: 0.25em 0.25em 3px rgba(0,0,128,0.5); + } +</style> +<p>the target should have a shadow</p> +<script> + window.location.hash = "#:~:text=target"; +</script> diff --git a/tests/wpt/tests/css/css-ruby/line-spacing.html b/tests/wpt/tests/css/css-ruby/line-spacing.html index 9d3c6f23e7d..4854e984c44 100644 --- a/tests/wpt/tests/css/css-ruby/line-spacing.html +++ b/tests/wpt/tests/css/css-ruby/line-spacing.html @@ -149,5 +149,16 @@ test(() => { assert_greater_than_equal(thirdLine.top - rubyLine.top, rubyLine.top - firstLine.top + RUBY_EMPHASIS_SIZE); }, 'Don\'t Consume half-leading of the next line with text-emphasis'); + +// crbug.com/336592423 +test(() => { + const {container, ruby, rt} = renderRuby( + '<div style="line-height:1;">' + + '<span style="display:inline-block; width:1em; height:4em; vertical-align:top"></span><br>' + + '<ruby>base<rt>annotation</rt></ruby></div>'); + const firstLine = container.querySelector('span').getBoundingClientRect(); + const rtBox = rt.getBoundingClientRect(); + assert_greater_than_equal(rtBox.top, firstLine.bottom); +}, 'An atomic-inline should not overlap with an annotation in the next line'); </script> </body> diff --git a/tests/wpt/tests/css/css-shadow-parts/part-after-combinator-invalidation-ref.html b/tests/wpt/tests/css/css-shadow-parts/part-after-combinator-invalidation-ref.html new file mode 100644 index 00000000000..d6db0d0e4c4 --- /dev/null +++ b/tests/wpt/tests/css/css-shadow-parts/part-after-combinator-invalidation-ref.html @@ -0,0 +1,3 @@ +<!doctype html> +<meta charset="utf-8"> +<div>What color am I?</div> diff --git a/tests/wpt/tests/css/css-shadow-parts/part-after-combinator-invalidation.html b/tests/wpt/tests/css/css-shadow-parts/part-after-combinator-invalidation.html new file mode 100644 index 00000000000..76beabe8855 --- /dev/null +++ b/tests/wpt/tests/css/css-shadow-parts/part-after-combinator-invalidation.html @@ -0,0 +1,26 @@ +<!doctype html> +<meta charset="utf-8"> +<link rel="help" href="https://drafts.csswg.org/css-shadow-parts"> +<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1891296"> +<link rel="author" href="mailto:emilio@crisal.io" title="Emilio Cobos Álvarez"> +<link rel="author" href="https://mozilla.org" title="Mozilla"> +<link rel="match" href="part-after-combinator-invalidation-ref.html"> +<style> +.inactive > ::part(content) { + color: red; +} +</style> +<div class="inactive"> + <div id="host"> + <template shadowrootmode="open"> + <div part="content">What color am I?</div> + </template> + </div> +</div> +<script> +onload = () => { + host.getBoundingClientRect(); + host.parentNode.className = ""; + host.getBoundingClientRect(); +}; +</script> diff --git a/tests/wpt/tests/css/css-sizing/aspect-ratio/support/2x2-green.webm b/tests/wpt/tests/css/css-sizing/aspect-ratio/support/2x2-green.webm Binary files differindex 74af43afebe..d1c021c03d7 100644 --- a/tests/wpt/tests/css/css-sizing/aspect-ratio/support/2x2-green.webm +++ b/tests/wpt/tests/css/css-sizing/aspect-ratio/support/2x2-green.webm diff --git a/tests/wpt/tests/css/css-transitions/shadow-root-insertion.html b/tests/wpt/tests/css/css-transitions/shadow-root-insertion.html new file mode 100644 index 00000000000..47fc665aa39 --- /dev/null +++ b/tests/wpt/tests/css/css-transitions/shadow-root-insertion.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="utf-8"> +<title>CSS Transitions: behavior when a shadow root is inserted while transitioning</title> +<meta name="assert" content="Checks the addition of a shadow root does not affect an in-flight transition"> +<link rel="help" href="https://drafts.csswg.org/css-transitions/"> + +<script src="/resources/testharness.js" type="text/javascript"></script> +<script src="/resources/testharnessreport.js" type="text/javascript"></script> +<script src="./support/helper.js" type="text/javascript"></script> + +</head> +<body> +<div id="log"></div> +<script> +test(t => { + // Start a 100s transition 50% of the way through + const div = addDiv(t, { + style: 'transition: height 100s -50s linear; height: 0px', + }); + getComputedStyle(div).height; + div.style.height = '100px'; + assert_equals( + getComputedStyle(div).height, + '50px', + 'Transition should be initially 50% complete' + ); + + // Add a shadow root + div.attachShadow({ mode: "open" }); + + // The transition on the height property should not have been canceled + assert_equals( + getComputedStyle(div).height, + '50px', + 'Transition should not have been canceled' + ); +}, 'addition of a shadow root should not cancel in-flight transitions'); +</script> + +</body> +</html> diff --git a/tests/wpt/tests/css/css-typed-om/the-stylepropertymap/properties/animation-delay-end.tentative.html b/tests/wpt/tests/css/css-typed-om/the-stylepropertymap/properties/animation-delay-end.tentative.html deleted file mode 100644 index 79b9f946174..00000000000 --- a/tests/wpt/tests/css/css-typed-om/the-stylepropertymap/properties/animation-delay-end.tentative.html +++ /dev/null @@ -1,19 +0,0 @@ -<!DOCTYPE html> -<title>'animation-delay-end' property</title> -<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get"> -<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set"> -<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization"> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="../../resources/testhelper.js"></script> -<script src="resources/testsuite.js"></script> -<body> -<div id="log"></div> -<script> -'use strict'; - -runListValuedPropertyTests('animation-delay-end', [ - { syntax: '<time>' } -]); - -</script> diff --git a/tests/wpt/tests/css/css-typed-om/the-stylepropertymap/properties/animation-delay-start.tentative.html b/tests/wpt/tests/css/css-typed-om/the-stylepropertymap/properties/animation-delay-start.tentative.html deleted file mode 100644 index 2fba4d8e514..00000000000 --- a/tests/wpt/tests/css/css-typed-om/the-stylepropertymap/properties/animation-delay-start.tentative.html +++ /dev/null @@ -1,19 +0,0 @@ -<!DOCTYPE html> -<title>'animation-delay-start' property</title> -<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-get"> -<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-set"> -<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#property-stle-value-normalization"> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="../../resources/testhelper.js"></script> -<script src="resources/testsuite.js"></script> -<body> -<div id="log"></div> -<script> -'use strict'; - -runListValuedPropertyTests('animation-delay-start', [ - { syntax: '<time>' } -]); - -</script> diff --git a/tests/wpt/tests/css/css-values/calc-rounding-003-ref.html b/tests/wpt/tests/css/css-values/calc-rounding-003-ref.html new file mode 100644 index 00000000000..e80dd7b95c8 --- /dev/null +++ b/tests/wpt/tests/css/css-values/calc-rounding-003-ref.html @@ -0,0 +1,10 @@ +<!doctype html> +<meta charset="utf-8"> +<style> + .outer { + width: 100px; + height: 40px; + border: 1px solid; + } +</style> +<div class="outer"></div> diff --git a/tests/wpt/tests/css/css-values/calc-rounding-003.html b/tests/wpt/tests/css/css-values/calc-rounding-003.html new file mode 100644 index 00000000000..bf609e1c1d4 --- /dev/null +++ b/tests/wpt/tests/css/css-values/calc-rounding-003.html @@ -0,0 +1,25 @@ +<!doctype html> +<meta charset="utf-8"> +<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1893127"> +<link rel="help" href="https://drafts.csswg.org/css-values/#funcdef-calc"> +<link rel="author" href="mailto:emilio@crisal.io" title="Emilio Cobos Álvarez"> +<link rel="author" href="https://mozilla.org" title="Mozilla"> +<link rel="match" href="calc-rounding-003-ref.html"> +<style> + .outer { + width: 100px; + border: 1px solid; + } + .inner { + height: 40px; + vertical-align: top; + display: inline-block; + --margin: 4.009px; + width: calc(50% - 2 * var(--margin)); + margin-inline: var(--margin); + } +</style> +<div class="outer" + ><div class="inner"></div + ><div class="inner"></div +></div> diff --git a/tests/wpt/tests/css/css-values/calc-size/animation/calc-size-height-interpolation.tentative.html b/tests/wpt/tests/css/css-values/calc-size/animation/calc-size-height-interpolation.tentative.html index 06277376e9f..6d15c3f2265 100644 --- a/tests/wpt/tests/css/css-values/calc-size/animation/calc-size-height-interpolation.tentative.html +++ b/tests/wpt/tests/css/css-values/calc-size/animation/calc-size-height-interpolation.tentative.html @@ -180,7 +180,7 @@ test_interpolation({ property: 'height', from: 'calc-size(37px, 200px)', - to: `calc-size(37px, size * 2 + 3% + 17px)`, /* adds to 100px */ + to: 'calc-size(37px, size * 2 + 3% + 17px)', /* adds to 100px */ }, [ { at: -0.25, expect: '225px' }, { at: 0, expect: '200px' }, @@ -189,4 +189,43 @@ { at: 1.25, expect: '75px' }, ]); + test_interpolation({ + property: 'height', + from: 'calc-size(auto, size)', + to: '50%', + }, [ + { at: -0.25, expect: '87.5px' }, + { at: 0, expect: '100px' }, + { at: 0.75, expect: '137.5px' }, + { at: 1, expect: '150px' }, + { at: 1.25, expect: '162.5px' }, + ]); + + // Rerun roughly the same test with an auto height container. + let auto_style_text = ` + .parent { + height: auto; + } + `; + let auto_style_element = document.createElement("style"); + auto_style_element.append(document.createTextNode(auto_style_text)); + document.head.append(auto_style_element); + + test_interpolation({ + property: 'height', + from: 'calc-size(auto, size * 2)', + to: '50%', + }, [ + { at: -0.25, expect: '250px' }, + { at: 0, expect: '200px' }, + { at: 0.75, expect: '50px' }, + /* TODO(https://crbug.com/40339056): It's questionable whether this should + * be the case, particularly for transitions. Perhaps the value at the + * end should have its percentage-ness back and be 100px here? */ + { at: 1, expect: '0px' }, + { at: 1.25, expect: '0px' }, + ]); + + auto_style_element.remove(); + </script> diff --git a/tests/wpt/tests/css/css-values/calc-size/calc-size-parsing.tentative.html b/tests/wpt/tests/css/css-values/calc-size/calc-size-parsing.tentative.html index afcb200424f..422ab3c33ee 100644 --- a/tests/wpt/tests/css/css-values/calc-size/calc-size-parsing.tentative.html +++ b/tests/wpt/tests/css/css-values/calc-size/calc-size-parsing.tentative.html @@ -70,4 +70,16 @@ test_valid_value("width", "calc-size(calc-size(min-content, size), size)"); test_invalid_value("height", "calc(12% + calc-size(any, 31%))"); +// Based on the discussion in https://github.com/w3c/csswg-drafts/issues/10259 +// this presumes parse-time conversion of the one-argument form to the +// two-argument form, but this isn't yet specified. +test_valid_value("width", "calc-size(30px)", "calc-size(any, 30px)"); +test_valid_value("width", "calc-size(min(30px, 2em))", "calc-size(any, min(30px, 2em))"); +test_invalid_value("width", "calc-size(any)"); +test_valid_value("width", "calc-size(calc-size(any, 30px))", "calc-size(calc-size(any, 30px), size)"); +test_invalid_value("width", "calc-size(size)"); +test_valid_value("width", "calc-size(fit-content)", "calc-size(fit-content, size)"); +test_valid_value("width", "calc-size(calc-size(fit-content, size * 2))", "calc-size(calc-size(fit-content, size * 2), size)"); +test_valid_value("width", "calc-size(calc-size(30px))", "calc-size(calc-size(any, 30px), size)"); + </script> diff --git a/tests/wpt/tests/css/css-values/container-progress-computed.tentative.html b/tests/wpt/tests/css/css-values/container-progress-computed.tentative.html index 9ab537cad6c..5c8d12f9cd4 100644 --- a/tests/wpt/tests/css/css-values/container-progress-computed.tentative.html +++ b/tests/wpt/tests/css/css-values/container-progress-computed.tentative.html @@ -1,4 +1,5 @@ <!DOCTYPE html> +<meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="help" href="https://drafts.csswg.org/css-values-5/#container-progress-func"> <link rel="author" title="sakhapov@chromium.org"> <script src="/resources/testharness.js"></script> @@ -15,6 +16,8 @@ <style> :root { font-size: 10px; + width: 100vw; + height: 100vh; } #out-of-scope-container { container: my-container-3 / size; @@ -42,8 +45,9 @@ </style> <script> -let width = window.innerWidth; -let height = window.innerHeight; +// innerWidth and innerHeight have lossy precision, see +// https://github.com/w3c/csswg-drafts/issues/5260. +let { width, height } = document.documentElement.getBoundingClientRect(); let extraWidth = 5051; let extraHeight = 1337; @@ -68,10 +72,10 @@ test_math_used('calc(container-progress(width of my-container from 0px to 50px) test_math_used('calc(container-progress(height of my-container from 10px to sign(50px - 500em) * 10px))', (outerHeight - 10) / (-10 - 10), {type:'number'}); // Fallback -test_math_used('container-progress(width of non-existing-container from 0px to 1px)', width, {type:'number'}); -test_math_used('container-progress(height of non-existing-container from 0px to 1px)', height, {type:'number'}); -test_math_used('container-progress(width of out-of-scope-container from 0px to 1px)', width, {type:'number'}); -test_math_used('container-progress(height of out-of-scope-container from 0px to 1px)', height, {type:'number'}); +test_math_used('container-progress(width of non-existing-container from 0px to 1px)', width, {type:'number', msg: 'container-progress() width fallback for non-existing container name'}); +test_math_used('container-progress(height of non-existing-container from 0px to 1px)', height, {type:'number', msg: 'container-progress() height fallback for non-existing container names'}); +test_math_used('container-progress(width of out-of-scope-container from 0px to 1px)', width, {type:'number', msg: 'container-progress() width fallback for out of scope container'}); +test_math_used('container-progress(height of out-of-scope-container from 0px to 1px)', height, {type:'number', msg: 'container-progress() height fallback for out of scope container'}); // Type checking test_math_used('calc(container-progress(width from 0px to 1px) * 1px)', innerWidth + 'px'); diff --git a/tests/wpt/tests/css/css-view-transitions/iframe-transition-destroyed-document-crash.html b/tests/wpt/tests/css/css-view-transitions/iframe-transition-destroyed-document-crash.html index 31f6a10ed62..13e743c3a17 100644 --- a/tests/wpt/tests/css/css-view-transitions/iframe-transition-destroyed-document-crash.html +++ b/tests/wpt/tests/css/css-view-transitions/iframe-transition-destroyed-document-crash.html @@ -1,18 +1,17 @@ <!DOCTYPE html> -<html class=reftest-wait> +<html class=test-wait> <title>View transitions: crash test</title> <link rel="help" href="https://www.w3.org/TR/css-view-transitions-1/"> <link rel="author" href="mailto:vmpstr@chromium.org"> -<script src="/common/reftest-wait.js"></script> <html> <head> <script> function eventhandler1() { - var var00106 = htmlvar00011.contentDocument; - var var00228 = var00106.startViewTransition(); + var iframeDoc = iframe.contentDocument; + var viewTransition = iframeDoc.startViewTransition(); requestAnimationFrame(() => { requestAnimationFrame(() => { - requestAnimationFrame(takeScreenshot); + requestAnimationFrame(() => document.documentElement.classList.remove("test-wait")); }) }); } @@ -20,8 +19,7 @@ function eventhandler1() { </script> </head> <body> -<iframe id="htmlvar00011" onunload="eventhandler3()" border="0" srcdoc="A#:^;<gV<>8" style=":{,J" referrerpolicy="unsafe-url" background="!xp>" nohref="nohref" nonce="nonce" inputEncoding="s" offsetX="0.3538512271910753">:+j&;&-^>.7xf\jZ1,xb</iframe> -<style id="htmlvar00014" nonce="nonce" media="screen and (min-width:0px)" onerror="eventhandler1()" onload="eventhandler1()" dir="N5!" updateRangeEnd="0" abbr="4IvGMN[Wxd" symbols="=d##y#)DA4V8ya}KO.cv" frameBorder="^b*]&:|#lB:" search="N">ynFXo*</style> +<iframe id="iframe" onunload="eventhandler3()" border="0" srcdoc="A#:^;<gV<>8" style=":{,J" referrerpolicy="unsafe-url" background="!xp>" nohref="nohref" nonce="nonce" inputEncoding="s" offsetX="0.3538512271910753">:+j&;&-^>.7xf\jZ1,xb</iframe> +<style nonce="nonce" media="screen and (min-width:0px)" onerror="eventhandler1()" onload="eventhandler1()" dir="N5!" updateRangeEnd="0" abbr="4IvGMN[Wxd" symbols="=d##y#)DA4V8ya}KO.cv" frameBorder="^b*]&:|#lB:" search="N">ynFXo*</style> </body> </html> - diff --git a/tests/wpt/tests/css/css-view-transitions/pseudo-element-overflow-hidden-ref.html b/tests/wpt/tests/css/css-view-transitions/pseudo-element-overflow-hidden-ref.html new file mode 100644 index 00000000000..02bcb5bb49c --- /dev/null +++ b/tests/wpt/tests/css/css-view-transitions/pseudo-element-overflow-hidden-ref.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<html style="background:pink"> +<title>View transitions: overflow:hidden is respected on pseudo elements</title> +<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/"> +<style> +body { + margin: 0px; +} +div { + width: 200px; + height: 200px; +} +#target { + position: absolute; + width: 200px; + height: 200px; + background: green; + overflow: hidden; +} +#inner { + position: relative; + left: 100px; + top: 100px; + background: blue; +} +.offset { + left: 400px; +} +</style> + +<div id="target"><div id="inner"></div></div> +<div id="target" class="offset"><div id="inner"></div></div> +</html> diff --git a/tests/wpt/tests/css/css-view-transitions/pseudo-element-overflow-hidden.html b/tests/wpt/tests/css/css-view-transitions/pseudo-element-overflow-hidden.html new file mode 100644 index 00000000000..e40df4f6a8b --- /dev/null +++ b/tests/wpt/tests/css/css-view-transitions/pseudo-element-overflow-hidden.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<title>View transitions: overflow:hidden is respected on pseudo elements</title> +<link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/"> +<link rel="author" href="mailto:mattwoodrow@apple.com"> +<link rel="match" href="pseudo-element-overflow-hidden-ref.html"> +<script src="/common/reftest-wait.js"></script> +<style> +body { + margin: 0px; +} +div { + width: 200px; + height: 200px; +} +#target { + width: 200px; + height: 200px; + background: green; + view-transition-name: target; +} +#inner { + position: relative; + left: 100px; + top: 100px; + background: blue; +} + +/* We're verifying what we capture, so just display both of the captures for 5 minutes. */ +html::view-transition-group(*) { animation-duration: 300s; } +html::view-transition-new(*) { animation: unset; opacity: 1; } +html::view-transition-old(*) { animation: unset; opacity: 1; } +/* hide the root so we show transition background to ensure we're in a transition */ +html::view-transition-group(root) { animation: unset; opacity: 0; } +html::view-transition { background: pink; } + +html::view-transition-new(target) { + overflow:hidden; +} +html::view-transition-old(target) { + left: 400px; + overflow: hidden; +} +</style> + +<div id="target"><div id="inner"></div></div> +<script> +failIfNot(document.startViewTransition, "Missing document.startViewTransition"); + +async function runTest() { + let t = document.startViewTransition(); + t.ready.then(takeScreenshot); +} +onload = () => requestAnimationFrame(() => requestAnimationFrame(runTest)); +</script> + +</html> diff --git a/tests/wpt/tests/css/css-view-transitions/root-element-display-none-during-transition-crash.html b/tests/wpt/tests/css/css-view-transitions/root-element-display-none-during-transition-crash.html index b9c384d94a9..d67bb256fdb 100644 --- a/tests/wpt/tests/css/css-view-transitions/root-element-display-none-during-transition-crash.html +++ b/tests/wpt/tests/css/css-view-transitions/root-element-display-none-during-transition-crash.html @@ -1,10 +1,9 @@ <!DOCTYPE html> -<html class=reftest-wait> +<html class=test-wait> <title>View transitions: entry animation from root display none</title> <link rel="help" href="https://drafts.csswg.org/css-view-transitions-1/"> <link rel="author" href="mailto:vmpstr@chromium.org"> -<script src="/common/reftest-wait.js"></script> <style> .hidden { display: none; @@ -15,16 +14,14 @@ </style> <script> -failIfNot(document.startViewTransition, "Missing document.startViewTransition"); - async function runTest() { transition = document.startViewTransition(); - transition.ready.then( + transition.ready.then(() => { requestAnimationFrame(() => { document.documentElement.classList.toggle("hidden"); - })); - transition.finished.then(takeScreenshot); + }); + }); + transition.finished.then(() => document.documentElement.classList.remove("test-wait")); } onload = () => requestAnimationFrame(() => requestAnimationFrame(runTest)); </script> - diff --git a/tests/wpt/tests/css/css-viewport/zoom/scroll-top-test-with-zoom.html b/tests/wpt/tests/css/css-viewport/zoom/scroll-top-test-with-zoom.html index 290fa602af6..9656fe120e8 100644 --- a/tests/wpt/tests/css/css-viewport/zoom/scroll-top-test-with-zoom.html +++ b/tests/wpt/tests/css/css-viewport/zoom/scroll-top-test-with-zoom.html @@ -23,9 +23,7 @@ }, "Initial scrollTop with no zoom"); document.body.style.zoom = 1.2; - test(function() { - assert_approx_equals(container.scrollTop, 77, 0.99, "scrollTop should remain consistent within 1 px after zooming in"); - }, "scrollTop after increasing zoom level"); + document.body.offsetTop; document.body.style.zoom = 1; test(function() { diff --git a/tests/wpt/tests/css/cssom/WEB_FEATURES.yml b/tests/wpt/tests/css/cssom/WEB_FEATURES.yml new file mode 100644 index 00000000000..def314c45c7 --- /dev/null +++ b/tests/wpt/tests/css/cssom/WEB_FEATURES.yml @@ -0,0 +1,7 @@ +features: +- name: constructed-stylesheets + files: + - adoptedstylesheets-observablearray.html + - CSSStyleSheet-constructable-* + - CSSStyleSheet-constructable.html + - CSSStyleSheet-template-adoption.html diff --git a/tests/wpt/tests/css/mediaqueries/WEB_FEATURES.yml b/tests/wpt/tests/css/mediaqueries/WEB_FEATURES.yml new file mode 100644 index 00000000000..88f1510e1b4 --- /dev/null +++ b/tests/wpt/tests/css/mediaqueries/WEB_FEATURES.yml @@ -0,0 +1,5 @@ +features: +- name: prefers-color-scheme + files: + - prefers-color-scheme.html + - prefers-color-scheme-* diff --git a/tests/wpt/tests/css/css-color-adjust/rendering/dark-color-scheme/svg-as-image-ref.html b/tests/wpt/tests/css/mediaqueries/prefers-color-scheme-svg-as-image-ref.html index 1ff9d88f302..012513ef9fa 100644 --- a/tests/wpt/tests/css/css-color-adjust/rendering/dark-color-scheme/svg-as-image-ref.html +++ b/tests/wpt/tests/css/mediaqueries/prefers-color-scheme-svg-as-image-ref.html @@ -6,14 +6,14 @@ <style> div { background-color: blue; - height: 100px; - width: 100px; + height: 32px; + width: 32px; } @media (prefers-color-scheme: dark) { div { - background-color: green; + background-color: purple; } } </style> -<p>There should be green square below when the preferred color-scheme is dark, and blue otherwise.</p> +<p>There should be a purple square below when the preferred color-scheme is dark, and blue otherwise.</p> <div></div> diff --git a/tests/wpt/tests/css/mediaqueries/prefers-color-scheme-svg-as-image.html b/tests/wpt/tests/css/mediaqueries/prefers-color-scheme-svg-as-image.html new file mode 100644 index 00000000000..65d6556f7af --- /dev/null +++ b/tests/wpt/tests/css/mediaqueries/prefers-color-scheme-svg-as-image.html @@ -0,0 +1,8 @@ +<!doctype html> +<head> + <title>prefers-color-scheme inside an SVG image</title> + <link rel="help" href="https://www.w3.org/TR/mediaqueries-5/#descdef-media-prefers-color-scheme"> + <link rel="match" href="prefers-color-scheme-svg-as-image-ref.html"> +</head> +<p>There should be a purple square below when the preferred color-scheme is dark, and blue otherwise.</p> +<img src='resources/prefers-color-scheme.svg'> diff --git a/tests/wpt/tests/css/selectors/WEB_FEATURES.yml b/tests/wpt/tests/css/selectors/WEB_FEATURES.yml index 4b29277607f..2449bf4808f 100644 --- a/tests/wpt/tests/css/selectors/WEB_FEATURES.yml +++ b/tests/wpt/tests/css/selectors/WEB_FEATURES.yml @@ -5,6 +5,9 @@ features: - name: has files: - has-* +- name: read-write-pseudos + files: + - selector-read-write-* - name: user-pseudos files: - user-invalid.html diff --git a/tests/wpt/tests/css/selectors/user-invalid.html b/tests/wpt/tests/css/selectors/user-invalid.html index 5f8fa5811af..c57e7648312 100644 --- a/tests/wpt/tests/css/selectors/user-invalid.html +++ b/tests/wpt/tests/css/selectors/user-invalid.html @@ -29,6 +29,7 @@ <input placeholder="Required field" required id="required-input"><br> <textarea placeholder="Required field" required id="required-textarea"></textarea><br> <input type="checkbox" required id="required-checkbox"><br> + <input type="date" required id="required-date"><br> <input type="submit" id="submit-button"> <input type="reset" id="reset-button"> </form> @@ -80,12 +81,14 @@ promise_test(async () => { const requiredInput = document.querySelector("#required-input"); const requiredTextarea = document.querySelector("#required-textarea"); const requiredCheckbox = document.querySelector("#required-checkbox"); + const requiredDate = document.querySelector("#required-date"); const submitButton = document.querySelector("#submit-button"); const resetButton = document.querySelector("#reset-button"); assert_false(requiredInput.validity.valid); assert_false(requiredTextarea.validity.valid); assert_false(requiredCheckbox.validity.valid); + assert_false(requiredDate.validity.valid); // The selector can't match because no interaction has happened. assert_false(requiredInput.matches(":user-valid"), "Initially does not match :user-valid"); assert_false(requiredInput.matches(":user-invalid"), "Initially does not match :user-invalid"); @@ -96,6 +99,9 @@ promise_test(async () => { assert_false(requiredCheckbox.matches(":user-valid"), "Initially does not match :user-valid"); assert_false(requiredCheckbox.matches(":user-invalid"), "Initially does not match :user-invalid"); + assert_false(requiredDate.matches(":user-valid"), "Initially does not match :user-valid"); + assert_false(requiredDate.matches(":user-invalid"), "Initially does not match :user-invalid"); + submitButton.click(); assert_true(requiredInput.matches(":user-invalid"), "Submitted the form, input is validated"); @@ -107,6 +113,9 @@ promise_test(async () => { assert_true(requiredCheckbox.matches(":user-invalid"), "Submitted the form, checkbox is validated"); assert_false(requiredCheckbox.matches(":user-valid"), "Submitted the form, checkbox is validated"); + assert_true(requiredDate.matches(":user-invalid"), "Submitted the form, date input is validated"); + assert_false(requiredDate.matches(":user-valid"), "Submitted the form, date input is validated"); + resetButton.click(); assert_false(requiredInput.matches(":user-valid"), "Reset the form, user-interacted flag is reset"); @@ -118,6 +127,9 @@ promise_test(async () => { assert_false(requiredCheckbox.matches(":user-valid"), "Reset the form, user-interacted flag is reset"); assert_false(requiredCheckbox.matches(":user-invalid"), "Reset the form, user-interacted flag is reset"); + assert_false(requiredDate.matches(":user-valid"), "Reset the form, user-interacted flag is reset"); + assert_false(requiredDate.matches(":user-invalid"), "Reset the form, user-interacted flag is reset"); + // Test programmatic form submission with constraint validation. form.requestSubmit(); @@ -129,6 +141,9 @@ promise_test(async () => { assert_true(requiredCheckbox.matches(":user-invalid"), "Called form.requestSubmit(), checkbox is validated"); assert_false(requiredCheckbox.matches(":user-valid"), "Called form.requestSubmit(), checkbox is validated"); + + assert_true(requiredDate.matches(":user-invalid"), "Called form.requestSubmit(), date input is validated"); + assert_false(requiredDate.matches(":user-valid"), "Called form.requestSubmit(), date input is validated"); }, ":user-invalid selector properly interacts with submit & reset buttons"); // historical: https://github.com/w3c/csswg-drafts/issues/1329 @@ -193,4 +208,38 @@ promise_test(async () => { assert_true(checkbox.matches(':user-invalid'), 'Checkbox should match :user-invalid after clicking twice.'); }, 'A required checkbox should match :user-invalid if the user unchecks it and blurs.'); + +promise_test(async () => { + const date = document.getElementById('required-date'); + + const resetButton = document.getElementById('reset-button'); + resetButton.click(); + assert_false(date.matches(':user-invalid'), + 'date input should not match :user-invalid at the start of the test.'); + assert_equals(date.value, '', + 'date input should not have a value at the start of the test.'); + + date.value = '2024-04-15'; + assert_false(date.matches(':user-invalid'), + 'date should not match :user-invalid after programatically changing value.'); + date.value = ''; + assert_false(date.matches(':user-invalid'), + 'date should not match :user-invalid after programatically changing value.'); + + const tabKey = '\uE004'; + const backspace = '\uE003'; + date.focus(); + // Press tab twice at the end to make sure that focus has left the input. + await test_driver.send_keys(date, `1${tabKey}1${tabKey}1234${tabKey}${tabKey}`); + assert_not_equals(document.activeElement, date, + 'Pressing tab twice after typing in the date should have blurred the input.'); + assert_equals(date.value, '1234-01-01', + 'Date input value should match the testdriver input.'); + date.focus(); + await test_driver.send_keys(date, backspace); + assert_equals(date.value, '', + 'Date input value should be cleared when deleting one of the sub-values.'); + assert_true(date.matches(':user-invalid'), + 'Date input should match :user-invalid after typing in an invalid value.'); +}, 'A required date should match :user-invalid if the user unchecks it and blurs.'); </script> diff --git a/tests/wpt/tests/css/selectors/user-valid-user-invalid-multifield-inputs.tentative.html b/tests/wpt/tests/css/selectors/user-valid-user-invalid-multifield-inputs.tentative.html new file mode 100644 index 00000000000..d6358ea2691 --- /dev/null +++ b/tests/wpt/tests/css/selectors/user-valid-user-invalid-multifield-inputs.tentative.html @@ -0,0 +1,133 @@ +<!DOCTYPE html> +<link rel=author href="mailto:jarhar@chromium.org"> +<link rel="help" href="https://drafts.csswg.org/selectors/#user-pseudos"> +<link rel="help" href="https://html.spec.whatwg.org/#selector-user-invalid"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> + +<!-- This test asserts specifics of keyboard behavior in multifield inputs, + like type=date and type=time, in ways that are not specified. --> + +<form> + <input id=date type=date required> + <input id=time type=time required> + <input id=datetime-local type=datetime-local required> +</form> + +<script> +const tabKey = '\uE004'; +const backspace = '\uE003'; + +promise_test(async () => { + const date = document.getElementById('date'); + assert_false(date.matches(':user-valid'), + 'Date input should not match :user-valid at the start of the test.'); + assert_false(date.matches(':user-invalid'), + 'Date input should not match :user-invalid at the start of the test.'); + assert_equals(date.value, '', + 'Date input should not have a value at the start of the test.'); + + date.focus(); + await test_driver.send_keys(date, `1${tabKey}`); + assert_equals(date.value, '', + 'Date input value should not be set after partially inputting the date.'); + assert_false(date.matches(':user-valid'), + 'Date input should not match :user-valid after partially inputting the date.'); + assert_false(date.matches(':user-invalid'), + 'Date input should not match :user-invalid after partially inputting the date.'); + + await test_driver.send_keys(date, `1${tabKey}1234${tabKey}`); + assert_equals(date.value, '1234-01-01', + 'Date input value should match the testdriver input.'); + assert_true(date.matches(':user-valid'), + 'Date input should match :user-valid after typing in a complete value.'); + assert_false(date.matches(':user-invalid'), + 'Date input should not match :user-invalid after typing in a complete value.'); + + date.blur(); + date.focus(); + await test_driver.send_keys(date, backspace); + assert_equals(date.value, '', + 'Date input value should be cleared when deleting one of the sub-values.'); + assert_false(date.matches(':user-valid'), + 'Date input should not match :user-valid after typing in an invalid value.'); + assert_true(date.matches(':user-invalid'), + 'Date input should match :user-invalid after typing in an invalid value.'); +}, '<input type=date> keyboard behavior for :user-valid/:user-invalid.'); + +promise_test(async () => { + const time = document.getElementById('time'); + assert_false(time.matches(':user-valid'), + 'Time input should not match :user-valid at the start of the test.'); + assert_false(time.matches(':user-invalid'), + 'Time input should not match :user-invalid at the start of the test.'); + assert_equals(time.value, '', + 'Time input should not have a value at the start of the test.'); + + time.focus(); + await test_driver.send_keys(time, `1${tabKey}`); + assert_equals(time.value, '', + 'Time input value should not be set after partially inputting the time.'); + assert_false(time.matches(':user-valid'), + 'Time input should not match :user-valid after partially inputting the time.'); + assert_false(time.matches(':user-invalid'), + 'Time input should not match :user-invalid after partially inputting the time.'); + + await test_driver.send_keys(time, `2${tabKey}a${tabKey}`); + assert_equals(time.value, '01:02', + 'Time input value should match the testdriver input.'); + assert_true(time.matches(':user-valid'), + 'Time input should match :user-valid after typing in a complete value.'); + assert_false(time.matches(':user-invalid'), + 'Time input should not match :user-invalid after typing in a complete value.'); + + time.blur(); + time.focus(); + await test_driver.send_keys(time, backspace); + assert_equals(time.value, '', + 'Time input value should be cleared when deleting one of the sub-values.'); + assert_false(time.matches(':user-valid'), + 'Time input should not match :user-valid after typing in an invalid value.'); + assert_true(time.matches(':user-invalid'), + 'Time input should match :user-invalid after typing in an invalid value.'); +}, '<input type=time> keyboard behavior for :user-valid/:user-invalid.'); + +promise_test(async () => { + const dateTimeLocal = document.getElementById('datetime-local'); + assert_false(dateTimeLocal.matches(':user-valid'), + 'Datetime input should not match :user-valid at the start of the test.'); + assert_false(dateTimeLocal.matches(':user-invalid'), + 'Datetime input should not match :user-invalid at the start of the test.'); + assert_equals(dateTimeLocal.value, '', + 'Datetime input should not have a value at the start of the test.'); + + dateTimeLocal.focus(); + await test_driver.send_keys(dateTimeLocal, `1${tabKey}`); + assert_equals(dateTimeLocal.value, '', + 'Datetime input value should not be set after partially inputting the dateTimeLocal.'); + assert_false(dateTimeLocal.matches(':user-valid'), + 'Datetime input should not match :user-valid after partially inputting the dateTimeLocal.'); + assert_false(dateTimeLocal.matches(':user-invalid'), + 'Datetime input should not match :user-invalid after partially inputting the dateTimeLocal.'); + + await test_driver.send_keys(dateTimeLocal, `1${tabKey}1234${tabKey}1${tabKey}2${tabKey}a${tabKey}`); + assert_equals(dateTimeLocal.value, '1234-01-01T01:02', + 'Datetime input value should match the testdriver input.'); + assert_true(dateTimeLocal.matches(':user-valid'), + 'Datetime input should match :user-valid after typing in a complete value.'); + assert_false(dateTimeLocal.matches(':user-invalid'), + 'Datetime input should not match :user-invalid after typing in a complete value.'); + + dateTimeLocal.blur(); + dateTimeLocal.focus(); + await test_driver.send_keys(dateTimeLocal, backspace); + assert_equals(dateTimeLocal.value, '', + 'Datetime input value should be cleared when deleting one of the sub-values.'); + assert_false(dateTimeLocal.matches(':user-valid'), + 'Datetime input should not match :user-valid after typing in an invalid value.'); + assert_true(dateTimeLocal.matches(':user-invalid'), + 'Datetime input should match :user-invalid after typing in an invalid value.'); +}, '<input type=datetime-local> keyboard behavior for :user-valid/:user-invalid.'); +</script> diff --git a/tests/wpt/tests/css/selectors/user-valid.html b/tests/wpt/tests/css/selectors/user-valid.html index 7a12eb237d3..402003ba5e6 100644 --- a/tests/wpt/tests/css/selectors/user-valid.html +++ b/tests/wpt/tests/css/selectors/user-valid.html @@ -29,6 +29,7 @@ <input placeholder="Optional field" id="optional-input"><br> <textarea placeholder="Optional field" id="optional-textarea"></textarea><br> <input type="checkbox" id="optional-checkbox"><br> + <input type="date" id="optional-date"><br> <input required placeholder="Required field"> <!-- Prevent the form from navigating with this invalid input --> <input type="submit" id="submit-button"> <input type="reset" id="reset-button"> @@ -77,12 +78,14 @@ promise_test(async () => { const optionalInput = document.querySelector("#optional-input"); const optionalTextarea = document.querySelector("#optional-textarea"); const optionalCheckbox = document.querySelector("#optional-checkbox"); + const optionalDate = document.querySelector("#optional-date"); const submitButton = document.querySelector("#submit-button"); const resetButton = document.querySelector("#reset-button"); assert_true(optionalInput.validity.valid); assert_true(optionalTextarea.validity.valid); assert_true(optionalCheckbox.validity.valid); + assert_true(optionalDate.validity.valid); // The selector can't match because no interaction has happened. assert_false(optionalInput.matches(":user-valid"), "Initially does not match :user-valid"); assert_false(optionalInput.matches(":user-invalid"), "Initially does not match :user-invalid"); @@ -93,6 +96,9 @@ promise_test(async () => { assert_false(optionalCheckbox.matches(":user-valid"), "Initially does not match :user-valid"); assert_false(optionalCheckbox.matches(":user-invalid"), "Initially does not match :user-invalid"); + assert_false(optionalDate.matches(":user-valid"), "Initially does not match :user-valid"); + assert_false(optionalDate.matches(":user-invalid"), "Initially does not match :user-invalid"); + submitButton.click(); assert_true(optionalInput.matches(":user-valid"), "Submitted the form, input is validated"); @@ -104,6 +110,9 @@ promise_test(async () => { assert_true(optionalCheckbox.matches(":user-valid"), "Submitted the form, checkbox is validated"); assert_false(optionalCheckbox.matches(":user-invalid"), "Submitted the form, checkbox is validated"); + assert_true(optionalDate.matches(":user-valid"), "Submitted the form, date is validated"); + assert_false(optionalDate.matches(":user-invalid"), "Submitted the form, date is validated"); + resetButton.click(); assert_false(optionalInput.matches(":user-valid"), "Reset the form, user-interacted flag is reset"); @@ -115,6 +124,9 @@ promise_test(async () => { assert_false(optionalCheckbox.matches(":user-valid"), "Reset the form, user-interacted flag is reset"); assert_false(optionalCheckbox.matches(":user-invalid"), "Reset the form, user-interacted flag is reset"); + assert_false(optionalDate.matches(":user-valid"), "Reset the form, user-interacted flag is reset"); + assert_false(optionalDate.matches(":user-invalid"), "Reset the form, user-interacted flag is reset"); + // Test programmatic form submission with constraint validation. form.requestSubmit(); @@ -126,6 +138,9 @@ promise_test(async () => { assert_true(optionalCheckbox.matches(":user-valid"), "Called form.requestSubmit(), checkbox is validated"); assert_false(optionalCheckbox.matches(":user-invalid"), "Called form.requestSubmit(), checkbox is validated"); + + assert_true(optionalDate.matches(":user-valid"), "Called form.requestSubmit(), date is validated"); + assert_false(optionalDate.matches(":user-invalid"), "Called form.requestSubmit(), date is validated"); }, ":user-valid selector properly interacts with submit & reset buttons"); promise_test(async () => { @@ -151,4 +166,33 @@ promise_test(async () => { assert_true(checkbox.matches(':user-valid'), 'Checkbox should match :user-valid after clicking once.'); }, 'Checkboxes should match :user-valid after the user clicks on it.'); + +promise_test(async () => { + const date = document.getElementById('optional-date'); + + const resetButton = document.getElementById('reset-button'); + resetButton.click(); + assert_false(date.matches(':user-valid'), + 'Date input should not match :user-valid at the start of the test.'); + assert_equals(date.value, '', + 'Date input should not have a value at the start of the test.'); + + date.value = '2024-04-15'; + assert_false(date.matches(':user-valid'), + 'Date input should not match :user-valid after programatically changing value.'); + date.value = ''; + assert_false(date.matches(':user-valid'), + 'Date input should not match :user-valid after programatically changing value.'); + + const tabKey = '\uE004'; + date.focus(); + // Press tab twice at the end to make sure that focus has left the input. + await test_driver.send_keys(date, `1${tabKey}1${tabKey}1234${tabKey}${tabKey}`); + assert_not_equals(document.activeElement, date, + 'Pressing tab twice after typing in the date should have blurred the input.'); + assert_equals(date.value, '1234-01-01', + 'Date input value should match the testdriver input.'); + assert_true(date.matches(':user-valid'), + 'Date input should match :user-valid after typing in a value.'); +}, 'Date inputs should match :user-valid after the user types a value into it.'); </script> diff --git a/tests/wpt/tests/device-posture/device-posture-change-event.https.html b/tests/wpt/tests/device-posture/device-posture-change-event.https.html new file mode 100644 index 00000000000..eb2fc2d96f8 --- /dev/null +++ b/tests/wpt/tests/device-posture/device-posture-change-event.https.html @@ -0,0 +1,23 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script> +'use strict'; + +promise_test(async (t) => { + t.add_cleanup(async () => { + await test_driver.clear_device_posture(); + }); + const watcher = new EventWatcher(t, navigator.devicePosture, ['change']); + const postures = ['folded', 'continuous', 'folded']; + for (const posture of postures) { + await Promise.all([ + watcher.wait_for('change'), + test_driver.set_device_posture(posture) + ]); + assert_equals(posture, navigator.devicePosture.type); + } +}, 'Tests the Device Posture API change event handler.'); +</script> diff --git a/tests/wpt/tests/device-posture/device-posture-clear.https.html b/tests/wpt/tests/device-posture/device-posture-clear.https.html new file mode 100644 index 00000000000..319cd7266af --- /dev/null +++ b/tests/wpt/tests/device-posture/device-posture-clear.https.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script> +'use strict'; + +promise_test(async (t) => { + t.add_cleanup(async () => { + await test_driver.clear_device_posture(); + }); + const originalPosture = navigator.devicePosture.type; + const posture = originalPosture ? 'folded' : 'continuous'; + + const watcher = new EventWatcher(t, navigator.devicePosture, ['change']); + await Promise.all([ + watcher.wait_for('change'), + test_driver.set_device_posture(posture) + ]); + assert_equals(navigator.devicePosture.type, posture); + + await Promise.all([ + watcher.wait_for('change'), + test_driver.clear_device_posture() + ]); + assert_equals(navigator.devicePosture.type, originalPosture); +}, 'Tests that device posture override can be removed.'); +</script> diff --git a/tests/wpt/tests/device-posture/device-posture-event-listener.https.html b/tests/wpt/tests/device-posture/device-posture-event-listener.https.html new file mode 100644 index 00000000000..f4e21c89ccd --- /dev/null +++ b/tests/wpt/tests/device-posture/device-posture-event-listener.https.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script> +'use strict'; + +promise_test(async (t) => { + t.add_cleanup(async () => { + await test_driver.clear_device_posture(); + }); + assert_equals(navigator.devicePosture.type, 'continuous'); + + const promise = new Promise(resolve => { + navigator.devicePosture.addEventListener( + 'change', + () => { resolve(navigator.devicePosture.type); }, + { once: true } + ); + }); + await test_driver.set_device_posture('folded'); + assert_equals(await promise, 'folded'); +}, 'Tests the Device Posture API addEventListener change event handler.'); +</script> diff --git a/tests/wpt/tests/device-posture/device-posture-media-queries.https.html b/tests/wpt/tests/device-posture/device-posture-media-queries.https.html new file mode 100644 index 00000000000..e4dbd2e7d81 --- /dev/null +++ b/tests/wpt/tests/device-posture/device-posture-media-queries.https.html @@ -0,0 +1,27 @@ +<!DOCTYPE html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script> +'use strict'; + +promise_test(async (t) => { + t.add_cleanup(async () => { + await test_driver.clear_device_posture(); + }); + assert_equals(navigator.devicePosture.type, 'continuous'); + assert_true(matchMedia('(device-posture: continuous)').matches); + + const foldedMQL = window.matchMedia('(device-posture: folded)'); + const promise = new Promise(resolve => { + foldedMQL.addEventListener( + 'change', + () => { resolve(foldedMQL.matches); }, + { once: true } + ); + }); + await test_driver.set_device_posture('folded'); + assert_true(await promise); +}, 'Tests the Device Posture API Media Query change event handler.'); +</script> diff --git a/tests/wpt/tests/dom/nodes/moveBefore/tentative/continue-css-animation-left.html b/tests/wpt/tests/dom/nodes/moveBefore/tentative/continue-css-animation-left.html new file mode 100644 index 00000000000..8c7f73e3c93 --- /dev/null +++ b/tests/wpt/tests/dom/nodes/moveBefore/tentative/continue-css-animation-left.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<title>Node.moveBefore should preserve CSS animation state (left)</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<body> + <section id="old-parent"> + <div id="item"></div> + </section> + <section id="new-parent"> + </section> + <style> + @keyframes anim { + from { + left: 100px; + } + + to { + left: 400px; + } + } + + section { + position: absolute; + } + + #item { + position: relative; + width: 100px; + height: 100px; + background: green; + animation: 1s linear infinite alternate anim; + animation-delay: 100ms; + } + </style> + <script> + promise_test(async t => { + const item = document.querySelector("#item"); + let num_events = 0; + await new Promise(resolve => addEventListener("animationstart", () => { + num_events++; + resolve(); + })); + + // Reparent item + document.body.querySelector("#new-parent").moveBefore(item, null); + await new Promise(resolve => requestAnimationFrame(() => resolve())); + assert_equals(num_events, 1); + assert_not_equals(getComputedStyle(item).left, "0px"); + }); + </script> +</body> diff --git a/tests/wpt/tests/dom/nodes/moveBefore/tentative/continue-css-animation-transform.html b/tests/wpt/tests/dom/nodes/moveBefore/tentative/continue-css-animation-transform.html new file mode 100644 index 00000000000..e7a285893aa --- /dev/null +++ b/tests/wpt/tests/dom/nodes/moveBefore/tentative/continue-css-animation-transform.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<title>Node.moveBefore should preserve CSS animation state (transform)</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<body> + <section id="old-parent"> + <div id="item"></div> + </section> + <section id="new-parent"> + </section> + <style> + @keyframes anim { + from { + transform: translateX(100px); + } + + to { + transform: translateX(400px); + } + } + + #item { + position: relative; + width: 100px; + height: 100px; + background: green; + animation: 1s linear infinite alternate anim; + animation-delay: 100ms; + } + </style> + <script> + promise_test(async t => { + const item = document.querySelector("#item"); + let num_events = 0; + await new Promise(resolve => addEventListener("animationstart", () => { + num_events++; + resolve(); + })); + + // Reparent item + document.body.querySelector("#new-parent").moveBefore(item, null); + await new Promise(resolve => requestAnimationFrame(() => resolve())); + assert_equals(num_events, 1); + assert_not_equals(getComputedStyle(item).transform, "none"); + }); + </script> +</body> diff --git a/tests/wpt/tests/dom/nodes/moveBefore/tentative/continue-css-transition-left-pseudo.html b/tests/wpt/tests/dom/nodes/moveBefore/tentative/continue-css-transition-left-pseudo.html new file mode 100644 index 00000000000..fa51b168879 --- /dev/null +++ b/tests/wpt/tests/dom/nodes/moveBefore/tentative/continue-css-transition-left-pseudo.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<title>Node.moveBefore should preserve CSS transition state on pseudo-elements (left)</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<body> + <section id="old-parent"> + <div id="item"></div> + </section> + <section id="new-parent"> + </section> + <style> + #item { + width: 100px; + height: 100px; + background: green; + position: absolute; + left: 0; + } + + #item::before { + content: "Foo"; + width: 100px; + height: 100px; + background: green; + transition: left 60s steps(1, jump-both); + left: 0px; + position: absolute; + } + + #item.big::before { + left: 400px; + } + + section { + position: relative; + } + + body { + margin-left: 0; + } + </style> + <script> + promise_test(async t => { + const item = document.querySelector("#item"); + assert_equals(item.getBoundingClientRect().x, 0); + item.classList.add("big"); + await new Promise(resolve => item.addEventListener("transitionstart", resolve)); + document.querySelector("#new-parent").moveBefore(item, null); + await new Promise(resolve => requestAnimationFrame(() => resolve())); + assert_equals(getComputedStyle(item, "::before").left, "200px"); + }); + </script> +</body> diff --git a/tests/wpt/tests/dom/nodes/moveBefore/tentative/continue-css-transition-left.html b/tests/wpt/tests/dom/nodes/moveBefore/tentative/continue-css-transition-left.html new file mode 100644 index 00000000000..2b8e04b26e4 --- /dev/null +++ b/tests/wpt/tests/dom/nodes/moveBefore/tentative/continue-css-transition-left.html @@ -0,0 +1,41 @@ +<!DOCTYPE html> +<title>Node.moveBefore should preserve CSS transition state (left)</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<body> + <section id="old-parent"> + <div id="item"></div> + </section> + <section id="new-parent"> + </section> + <style> + #item { + width: 100px; + height: 100px; + background: green; + transition: left 10s; + position: absolute; + left: 0; + } + + section { + position: relative; + } + + body { + margin-left: 0; + } + </style> + <script> + promise_test(async t => { + const item = document.querySelector("#item"); + assert_equals(item.getBoundingClientRect().x, 0); + item.style.left = "400px"; + await new Promise(resolve => item.addEventListener("transitionstart", resolve)); + document.querySelector("#new-parent").moveBefore(item, null); + await new Promise(resolve => requestAnimationFrame(() => resolve())); + assert_less_than(item.getBoundingClientRect().x, 399); + }); + </script> +</body> diff --git a/tests/wpt/tests/dom/nodes/moveBefore/tentative/continue-css-transition-transform-pseudo.html b/tests/wpt/tests/dom/nodes/moveBefore/tentative/continue-css-transition-transform-pseudo.html new file mode 100644 index 00000000000..d02c72561c1 --- /dev/null +++ b/tests/wpt/tests/dom/nodes/moveBefore/tentative/continue-css-transition-transform-pseudo.html @@ -0,0 +1,54 @@ +<!DOCTYPE html> +<title>Node.moveBefore should preserve CSS transition state on pseudo-elements (transform)</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<body> + <section id="old-parent"> + <div id="item"></div> + </section> + <section id="new-parent"> + </section> + <style> + #item { + width: 100px; + height: 100px; + background: green; + position: absolute; + left: 0; + } + + #item::before { + content: "Foo"; + width: 100px; + height: 100px; + background: green; + transition: transform 60s steps(1, jump-both); + transform: none; + position: absolute; + } + + #item.big::before { + transform: translateX(400px); + } + + section { + position: relative; + } + + body { + margin-left: 0; + } + </style> + <script> + promise_test(async t => { + const item = document.querySelector("#item"); + assert_equals(item.getBoundingClientRect().x, 0); + item.classList.add("big"); + await new Promise(resolve => item.addEventListener("transitionstart", resolve)); + document.querySelector("#new-parent").moveBefore(item, null); + await new Promise(resolve => requestAnimationFrame(() => resolve())); + assert_not_equals(getComputedStyle(item, "::before").transform, "none"); + }); + </script> +</body> diff --git a/tests/wpt/tests/dom/nodes/moveBefore/tentative/continue-css-transition-transform.html b/tests/wpt/tests/dom/nodes/moveBefore/tentative/continue-css-transition-transform.html new file mode 100644 index 00000000000..f09edca1449 --- /dev/null +++ b/tests/wpt/tests/dom/nodes/moveBefore/tentative/continue-css-transition-transform.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<title>Node.moveBefore should preserve CSS transition state (transform)</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<body> + <section id="old-parent"> + <div id="item"></div> + </section> + <section id="new-parent"> + </section> + <style> + #item { + width: 100px; + height: 100px; + background: green; + transition: transform 60s steps(1, jump-both); + } + + body { + margin-left: 0; + } + </style> + <script> + promise_test(async t => { + const item = document.querySelector("#item"); + assert_equals(item.getBoundingClientRect().x, 0); + item.style.transform = "translateX(400px)"; + await new Promise(resolve => item.addEventListener("transitionstart", resolve)); + document.querySelector("#new-parent").moveBefore(item, null); + await new Promise(resolve => requestAnimationFrame(() => resolve())); + assert_equals(item.getBoundingClientRect().x, 200); + assert_equals(item.getAnimations().length, 1); + }); + </script> +</body> diff --git a/tests/wpt/tests/dom/nodes/moveBefore/tentative/css-animation-commit-styles.html b/tests/wpt/tests/dom/nodes/moveBefore/tentative/css-animation-commit-styles.html new file mode 100644 index 00000000000..86bb7c33e42 --- /dev/null +++ b/tests/wpt/tests/dom/nodes/moveBefore/tentative/css-animation-commit-styles.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<title>Calling commitStyles after Node.moveBefore should commit mid-transition value</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<body> + <section id="old-parent"> + <div id="item"></div> + </section> + <section id="new-parent"> + </section> + <style> + @keyframes anim { + from { + transform: translateX(100px); + } + + to { + transform: translateX(400px); + } + } + + #item { + position: relative; + width: 100px; + height: 100px; + background: green; + animation: 1s linear infinite alternate anim; + animation-delay: 100ms; + } + </style> + <script> + promise_test(async t => { + const item = document.querySelector("#item"); + await new Promise(resolve => item.addEventListener("animationstart", resolve)); + + // Reparent item + document.body.querySelector("#new-parent").moveBefore(item, null); + + item.getAnimations()[0].commitStyles(); + assert_true("transform" in item.style); + assert_not_equals(item.style.transform, "none"); + }); + </script> +</body> diff --git a/tests/wpt/tests/dom/nodes/moveBefore/tentative/css-transition-cross-document.html b/tests/wpt/tests/dom/nodes/moveBefore/tentative/css-transition-cross-document.html new file mode 100644 index 00000000000..f3c8fafbfa8 --- /dev/null +++ b/tests/wpt/tests/dom/nodes/moveBefore/tentative/css-transition-cross-document.html @@ -0,0 +1,45 @@ +<!DOCTYPE html> +<title>Node.moveBefore should not preserve CSS transition state when crossing document boundaries</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<body> + <iframe id="iframe"> + </iframe> + <section id="new-parent"> + </section> + <style id="style"> + #item { + width: 100px; + height: 100px; + background: green; + transition: left 10s; + position: absolute; + left: 0; + } + + section { + position: relative; + } + + body { + margin-left: 0; + } + </style> + <script> + promise_test(async t => { + const iframe = document.querySelector("#iframe"); + const style = document.querySelector("#style"); + iframe.contentDocument.head.append(style.cloneNode(true)); + const item = iframe.contentDocument.createElement("div"); + item.id = "item"; + iframe.contentDocument.body.append(item); + assert_equals(item.getBoundingClientRect().x, 0); + item.style.left = "400px"; + await new Promise(resolve => item.addEventListener("transitionstart", resolve)); + document.querySelector("#new-parent").moveBefore(item, null); + await new Promise(resolve => requestAnimationFrame(() => resolve())); + assert_greater_than(item.getBoundingClientRect().x, 399); + }, "Moving a transition across documents should reset its state"); + </script> +</body> diff --git a/tests/wpt/tests/dom/nodes/moveBefore/tentative/css-transition-cross-shadow.html b/tests/wpt/tests/dom/nodes/moveBefore/tentative/css-transition-cross-shadow.html new file mode 100644 index 00000000000..145f40ba504 --- /dev/null +++ b/tests/wpt/tests/dom/nodes/moveBefore/tentative/css-transition-cross-shadow.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<title>Node.moveBefore should not preserve CSS transition state when crossing shadow boundaries</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<body> + <section id="old-parent"> + <div id="item"></div> + </section> + <div id="shadow-container"> + <template shadowrootmode="open"> + <style> + #item { + width: 100px; + height: 100px; + background: green; + transition: left 10s; + position: absolute; + left: 0; + } + + section { + position: relative; + } + + body { + margin-left: 0; + } + </style> + <section id="new-parent"> + </section> + </template> + </div> + <style> + #item { + width: 100px; + height: 100px; + background: green; + transition: left 10s; + position: absolute; + left: 0; + } + + section { + position: relative; + } + + body { + margin-left: 0; + } + </style> + <script> + promise_test(async t => { + const item = document.querySelector("#item"); + assert_equals(item.getBoundingClientRect().x, 0); + item.style.left = "400px"; + await new Promise(resolve => item.addEventListener("transitionstart", resolve)); + const shadowContainer = document.querySelector("#shadow-container"); + shadowContainer.shadowRoot.querySelector("#new-parent").moveBefore(item, null); + await new Promise(resolve => requestAnimationFrame(() => resolve())); + assert_greater_than(item.getBoundingClientRect().x, 399); + }, "Moving an element with a transition across shadow boundaries should reset the transition"); + </script> +</body> diff --git a/tests/wpt/tests/dom/nodes/moveBefore/tentative/css-transition-to-disconnected-document.html b/tests/wpt/tests/dom/nodes/moveBefore/tentative/css-transition-to-disconnected-document.html new file mode 100644 index 00000000000..537edfe9b61 --- /dev/null +++ b/tests/wpt/tests/dom/nodes/moveBefore/tentative/css-transition-to-disconnected-document.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<title>Node.moveBefore should act like insertBefore when moving to a disconnected document</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<body> + <section id="old-parent"> + <div id="item"></div> + </section> + <section id="new-parent"> + </section> + <style> + #item { + width: 100px; + height: 100px; + background: green; + transition: left 10s; + position: absolute; + left: 0; + } + + section { + position: relative; + } + + body { + margin-left: 0; + } + </style> + + <script> + promise_test(async t => { + const item = document.querySelector("#item"); + assert_equals(item.getBoundingClientRect().x, 0); + item.style.left = "400px"; + await new Promise(resolve => item.addEventListener("transitionstart", resolve)); + const doc = document.implementation.createHTMLDocument(); + doc.body.moveBefore(item, null); + await new Promise(resolve => requestAnimationFrame(() => resolve())); + assert_equals(item.getBoundingClientRect().x, 0); + }, "Moving an element with a transition to a disconnected document should reset the transitionm state"); + </script> +</body> diff --git a/tests/wpt/tests/dom/nodes/moveBefore/tentative/css-transition-trigger.html b/tests/wpt/tests/dom/nodes/moveBefore/tentative/css-transition-trigger.html new file mode 100644 index 00000000000..0cb5608a695 --- /dev/null +++ b/tests/wpt/tests/dom/nodes/moveBefore/tentative/css-transition-trigger.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<title>Node.moveBefore should trigger CSS transition state (left) if needed</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<body> + <section id="old-parent"> + <div id="item"></div> + </section> + <section id="new-parent"> + </section> + <style> + #item { + width: 100px; + height: 100px; + background: green; + transition: left 10s steps(1, jump-both); + position: absolute; + left: 0; + } + + #new-parent #item { + left: 400px; + } + + section { + position: relative; + } + + body { + margin-left: 0; + } + </style> + <script> + promise_test(async t => { + const item = document.querySelector("#item"); + assert_equals(item.getBoundingClientRect().x, 0); + document.querySelector("#new-parent").moveBefore(item, null); + await new Promise(resolve => item.addEventListener("transitionstart", resolve)); + assert_equals(item.getBoundingClientRect().x, 200); + }); + </script> +</body> diff --git a/tests/wpt/tests/dom/nodes/moveBefore/tentative/fullscreen-preserve.html b/tests/wpt/tests/dom/nodes/moveBefore/tentative/fullscreen-preserve.html new file mode 100644 index 00000000000..810eeac9af9 --- /dev/null +++ b/tests/wpt/tests/dom/nodes/moveBefore/tentative/fullscreen-preserve.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<title>Document#fullscreenElement</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/fullscreen/trusted-click.js"></script> +<section id="old_parent"> + <div id="item"></div> +</section> +<section id="new_parent"> + <div id="item"></div> +</section> +<script> + promise_test(async function (t) { + const item = document.querySelector("#item"); + + await trusted_click(); + + assert_equals( + document.fullscreenElement, + null, + "fullscreenElement before requestFullscreen()" + ); + + await item.requestFullscreen(); + assert_equals( + document.fullscreenElement, + item, + "fullscreenElement before moveBefore()" + ); + + document.querySelector("#new_parent").moveBefore(item, null); + + assert_equals( + document.fullscreenElement, + item, + "fullscreenElement after moveBefore()" + ); + + await Promise.all([document.exitFullscreen(), fullScreenChange()]); + + assert_equals( + document.fullscreenElement, + null, + "fullscreenElement after exiting fullscreen" + ); + }); +</script> diff --git a/tests/wpt/tests/dom/observable/tentative/observable-every.any.js b/tests/wpt/tests/dom/observable/tentative/observable-every.any.js new file mode 100644 index 00000000000..74a344b8f78 --- /dev/null +++ b/tests/wpt/tests/dom/observable/tentative/observable-every.any.js @@ -0,0 +1,250 @@ +promise_test(async () => { + const source = new Observable(subscriber => { + subscriber.next("good"); + subscriber.next("good"); + subscriber.next("good"); + subscriber.complete(); + }); + + const result = await source.every((value) => value === "good"); + + assert_true(result, "Promise resolves with true if all values pass the predicate"); +}, "every(): Promise resolves to true if all values pass the predicate"); + +promise_test(async () => { + const source = new Observable(subscriber => { + subscriber.next("good"); + subscriber.next("good"); + subscriber.next("bad"); + subscriber.complete(); + }); + + const result = await source.every((value) => value === "good"); + + assert_false(result, "Promise resolves with false if any value fails the predicate"); +}, "every(): Promise resolves to false if any value fails the predicate"); + +promise_test(async () => { + let tornDown = false; + let subscriberActiveAfterFailingPredicate = true; + const source = new Observable(subscriber => { + subscriber.addTeardown(() => tornDown = true); + subscriber.next("good"); + subscriber.next("good"); + subscriber.next("bad"); + subscriberActiveAfterFailingPredicate = subscriber.active; + subscriber.next("good"); + subscriber.complete(); + }); + + const result = await source.every((value) => value === "good"); + + assert_false(result, "Promise resolves with false if any value fails the predicate"); + assert_false(subscriberActiveAfterFailingPredicate, + "Subscriber becomes inactive because every() unsubscribed"); +}, "every(): Abort the subscription to the source if the predicate does not pass"); + +promise_test(async () => { + const logs = []; + + const source = createTestSubject({ + onSubscribe: () => logs.push("subscribed to source"), + onTeardown: () => logs.push("teardown"), + }); + + const resultPromise = source.every((value, index) => { + logs.push(`Predicate called with ${value}, ${index}`); + return true; + }); + + let promiseResolved = false; + + resultPromise.then(() => promiseResolved = true); + + assert_array_equals(logs, ["subscribed to source"], + "calling every() subscribes to the source immediately"); + + source.next("a"); + assert_array_equals(logs, [ + "subscribed to source", + "Predicate called with a, 0" + ], "Predicate called with the value and the index"); + + source.next("b"); + assert_array_equals(logs, [ + "subscribed to source", + "Predicate called with a, 0", + "Predicate called with b, 1", + ], "Predicate called with the value and the index"); + + // wait a tick, just to prove that you have to wait for complete to be called. + await Promise.resolve(); + + assert_false(promiseResolved, + "Promise should not resolve until after the source completes"); + + source.complete(); + assert_array_equals(logs, [ + "subscribed to source", + "Predicate called with a, 0", + "Predicate called with b, 1", + "teardown", + ], "Teardown function called immediately after the source completes"); + + const result = await resultPromise; + + assert_true(result, + "Promise resolves with true if all values pass the predicate"); +}, "every(): Lifecycle checks when all values pass the predicate"); + +promise_test(async () => { + const logs = []; + + const source = createTestSubject({ + onSubscribe: () => logs.push("subscribed to source"), + onTeardown: () => logs.push("teardown"), + }); + + const resultPromise = source.every((value, index) => { + logs.push(`Predicate called with ${value}, ${index}`); + return value === "good"; + }); + + let promiseResolved = false; + + resultPromise.then(() => promiseResolved = true); + + assert_array_equals(logs, ["subscribed to source"], + "calling every() subscribes to the source immediately"); + + source.next("good"); + source.next("good"); + assert_array_equals(logs, [ + "subscribed to source", + "Predicate called with good, 0", + "Predicate called with good, 1", + ], "Predicate called with the value and the index"); + + assert_false(promiseResolved, "Promise should not resolve until after the predicate fails"); + + source.next("bad"); + assert_array_equals(logs, [ + "subscribed to source", + "Predicate called with good, 0", + "Predicate called with good, 1", + "Predicate called with bad, 2", + "teardown", + ], "Predicate called with the value and the index, failing predicate immediately aborts subscription to source"); + + const result = await resultPromise; + + assert_false(result, "Promise resolves with false if any value fails the predicate"); +}, "every(): Lifecycle checks when any value fails the predicate"); + +promise_test(async () => { + const source = new Observable(subscriber => { + subscriber.complete(); + }); + + const result = await source.every(() => true); + + assert_true(result, + "Promise resolves with true if the observable completes without " + + "emitting a value"); +}, "every(): Resolves with true if the observable completes without " + + "emitting a value"); + +promise_test(async t => { + const error = new Error("error from source"); + const source = new Observable(subscriber => { + subscriber.error(error); + }); + + promise_rejects_exactly(t, error, source.every(() => true), + "Promise rejects with the error emitted from the source observable"); +}, "every(): Rejects with any error emitted from the source observable"); + +promise_test(async t => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.complete(); + }); + + const error = new Error("bad value"); + const promise = source.every(value => { + if (value <= 2) return true; + throw error; + }); + + promise_rejects_exactly(t, error, promise, "Promise rejects with the " + + "error thrown from the predicate"); +}, "every(): Rejects with any error thrown from the predicate"); + +promise_test(async () => { + const indices = []; + + const source = new Observable(subscriber => { + subscriber.next("a"); + subscriber.next("b"); + subscriber.next("c"); + subscriber.complete(); + }); + + const value = await source.every((value, index) => { + indices.push(index); + return true; + }); + + assert_array_equals(indices, [0, 1, 2]); + + assert_true(value, + "Promise resolves with true if all values pass the predicate"); +}, "every(): Index is passed into the predicate"); + +promise_test(async t => { + const source = new Observable(subscriber => {}); + + const controller = new AbortController(); + const promise = source.every(() => true, { signal: controller.signal }); + controller.abort(); + + promise_rejects_dom(t, 'AbortError', promise, "Promise rejects with a " + + "DOMException if the source Observable is aborted"); +}, "every(): Rejects with a DOMException if the source Observable is aborted"); + +function createTestSubject(options) { + const onTeardown = options?.onTeardown; + + const subscribers = new Set(); + const subject = new Observable(subscriber => { + options?.onSubscribe?.(); + subscribers.add(subscriber); + subscriber.addTeardown(() => subscribers.delete(subscriber)); + if (onTeardown) { + subscriber.addTeardown(onTeardown); + } + }); + + subject.next = (value) => { + for (const subscriber of Array.from(subscribers)) { + subscriber.next(value); + } + }; + subject.error = (error) => { + for (const subscriber of Array.from(subscribers)) { + subscriber.error(error); + } + }; + subject.complete = () => { + for (const subscriber of Array.from(subscribers)) { + subscriber.complete(); + } + }; + subject.subscriberCount = () => { + return subscribers.size; + }; + + return subject; +} diff --git a/tests/wpt/tests/dom/observable/tentative/observable-filter.any.js b/tests/wpt/tests/dom/observable/tentative/observable-filter.any.js index 8a49bcf4674..3c1a7d78248 100644 --- a/tests/wpt/tests/dom/observable/tentative/observable-filter.any.js +++ b/tests/wpt/tests/dom/observable/tentative/observable-filter.any.js @@ -103,3 +103,15 @@ test(() => { ['source teardown', 'source abort event', 'filter observable complete']); }, "filter(): Upon source completion, source Observable teardown sequence " + "happens after downstream filter complete() is called"); + +test(() => { + const source = new Observable(subscriber => { + subscriber.next('value1'); + subscriber.next('value2'); + subscriber.next('value3'); + }); + + const indices = []; + source.filter((value, index) => indices.push(index)).subscribe(); + assert_array_equals(indices, [0, 1, 2]); +}, "filter(): Index is passed correctly to predicate"); diff --git a/tests/wpt/tests/dom/observable/tentative/observable-find.any.js b/tests/wpt/tests/dom/observable/tentative/observable-find.any.js new file mode 100644 index 00000000000..0e09060fc5a --- /dev/null +++ b/tests/wpt/tests/dom/observable/tentative/observable-find.any.js @@ -0,0 +1,85 @@ +promise_test(async () => { + let inactiveAfterB = false; + const source = new Observable(subscriber => { + subscriber.next("a"); + subscriber.next("b"); + inactiveAfterB = !subscriber.active; + subscriber.next("c"); + subscriber.complete(); + }); + + const value = await source.find((value) => value === "b"); + + assert_equals(value, "b", "Promise resolves with the first value that passes the predicate"); + + assert_true(inactiveAfterB, "subscriber is inactive after the first value that passes the predicate"); +}, "find(): Promise resolves with the first value that passes the predicate"); + +promise_test(async () => { + const source = new Observable(subscriber => { + subscriber.next("a"); + subscriber.next("b"); + subscriber.next("c"); + subscriber.complete(); + }); + + const value = await source.find(() => false); + + assert_equals(value, undefined, "Promise resolves with undefined if no value passes the predicate"); +}, "find(): Promise resolves with undefined if no value passes the predicate"); + +promise_test(async t => { + const error = new Error("error from source"); + const source = new Observable(subscriber => { + subscriber.error(error); + }); + + promise_rejects_exactly(t, error, source.find(() => true), "Promise " + + "rejects with the error emitted from the source Observable"); +}, "find(): Promise rejects with the error emitted from the source Observable"); + +promise_test(async t => { + const source = new Observable(subscriber => { + subscriber.next("ignored"); + }); + + const error = new Error("thrown from predicate"); + promise_rejects_exactly(t, error, source.find(() => {throw error}), + "Promise rejects with any error thrown from the predicate"); +}, "find(): Promise rejects with any error thrown from the predicate"); + +promise_test(async () => { + let indices = []; + + const source = new Observable(subscriber => { + subscriber.next("a"); + subscriber.next("b"); + subscriber.next("c"); + subscriber.complete(); + }); + + const value = await source.find((value, index) => { + indices.push(index); + return false; + }); + + assert_equals(value, undefined, "Promise resolves with undefined if no value passes the predicate"); + + assert_array_equals(indices, [0, 1, 2], "find(): Passes the index of the value to the predicate"); +}, "find(): Passes the index of the value to the predicate"); + +promise_test(async t => { + const controller = new AbortController(); + const source = new Observable(subscriber => { + subscriber.next("a"); + subscriber.next("b"); + subscriber.next("c"); + subscriber.complete(); + }); + + controller.abort(); + const promise = source.find(() => true, { signal: controller.signal }); + + promise_rejects_dom(t, 'AbortError', promise, "Promise rejects with " + + "DOMException when the signal is aborted"); +}, "find(): Rejects with AbortError when the signal is aborted"); diff --git a/tests/wpt/tests/dom/observable/tentative/observable-inspect.any.js b/tests/wpt/tests/dom/observable/tentative/observable-inspect.any.js new file mode 100644 index 00000000000..8aff741d267 --- /dev/null +++ b/tests/wpt/tests/dom/observable/tentative/observable-inspect.any.js @@ -0,0 +1,412 @@ +// Because we test that the global error handler is called at various times. +setup({ allow_uncaught_exception: true }); + +test(() => { + const results = []; + let sourceSubscriptionCall = 0; + const source = new Observable(subscriber => { + sourceSubscriptionCall++; + results.push(`source subscribe ${sourceSubscriptionCall}`); + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.complete(); + }); + + let inspectSubscribeCall = 0; + const result = source.inspect({ + subscribe: () => { + inspectSubscribeCall++; + results.push(`inspect() subscribe ${inspectSubscribeCall}`); + }, + next: (value) => results.push(`inspect() next ${value}`), + error: (e) => results.push(`inspect() error ${e.message}`), + complete: () => results.push(`inspect() complete`), + }); + + result.subscribe({ + next: (value) => results.push(`result next ${value}`), + error: (e) => results.push(`result error ${e.message}`), + complete: () => results.push(`result complete`), + }); + + result.subscribe({ + next: (value) => results.push(`result next ${value}`), + error: (e) => results.push(`result error ${e.message}`), + complete: () => results.push(`result complete`), + }); + + assert_array_equals(results, + [ + "inspect() subscribe 1", + "source subscribe 1", + "inspect() next 1", + "result next 1", + "inspect() next 2", + "result next 2", + "inspect() next 3", + "result next 3", + "inspect() complete", + "result complete", + "inspect() subscribe 2", + "source subscribe 2", + "inspect() next 1", + "result next 1", + "inspect() next 2", + "result next 2", + "inspect() next 3", + "result next 3", + "inspect() complete", + "result complete", + ]); +}, "inspect(): Provides a pre-subscription subscribe callback"); + +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.complete(); + }); + + const results = []; + + const result = source.inspect({ + next: value => results.push(value), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + result.subscribe(); + result.subscribe(); + + assert_array_equals(results, [1, 2, 3, "complete", 1, 2, 3, "complete"]); +}, "inspect(): Provides a way to tap into the values and completions of the " + + "source observable using an observer"); + +test(() => { + const error = new Error("error from source"); + const source = new Observable(subscriber => subscriber.error(error)); + + const results = []; + + const result = source.inspect({ + next: value => results.push(value), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + let errorReported = null; + self.addEventListener('error', e => errorReported = e.error, {once: true}); + result.subscribe(); + + assert_array_equals(results, [error]); + assert_equals(errorReported, error, + "errorReported to global matches error from source Observable"); +}, "inspect(): Error handler does not stop error from being reported to the " + + "global, when subscriber does not pass error handler"); + +test(() => { + const error = new Error("error from source"); + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.error(error); + }); + + const results = []; + + const result = source.inspect({ + next: value => results.push(value), + error: e => results.push(e), + complete: () => results.push("complete"), + }); + + const observer = { + error: e => results.push(e), + }; + result.subscribe(observer); + result.subscribe(observer); + + assert_array_equals(results, [1, 2, 3, error, error, 1, 2, 3, error, error]); +}, "inspect(): Provides a way to tap into the values and errors of the " + + "source observable using an observer. Errors are passed through"); + +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.complete(); + }); + + const results = []; + + const result = source.inspect(value => results.push(value)); + + result.subscribe(); + result.subscribe(); + + assert_array_equals(results, [1, 2, 3, 1, 2, 3]); +}, "inspect(): ObserverCallback passed in"); + +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + }); + + const error = new Error("error from inspect() next handler"); + const result = source.inspect({ + next: (value) => { + if (value === 2) { + throw error; + } + }, + }); + + const results1 = []; + result.subscribe({ + next: (value) => results1.push(value), + error: (e) => results1.push(e), + complete: () => results1.push("complete"), + }); + + const results2 = []; + result.subscribe({ + next: (value) => results2.push(value), + error: (e) => results2.push(e), + complete: () => results2.push("complete"), + }); + + assert_array_equals(results1, [1, error]); + assert_array_equals(results2, [1, error]); +}, "inspect(): Throwing an error in the observer next handler is caught and " + + "sent to the error callback of the result observable"); + +test(() => { + const sourceError = new Error("error from source"); + const inspectError = new Error("error from inspect() error handler"); + + const source = new Observable(subscriber => { + subscriber.error(sourceError); + }); + + const result = source.inspect({ + error: () => { + throw inspectError; + }, + }); + + const results = []; + result.subscribe({ + next: () => results.push("next"), + error: (e) => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [inspectError]); +}, "inspect(): Throwing an error in the observer error handler in " + + "inspect() is caught and sent to the error callback of the result " + + "observable"); + +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.complete(); + }); + + const error = new Error("error from inspect() complete handler"); + const result = source.inspect({ + complete: () => { + throw error; + }, + }); + + const results = []; + result.subscribe({ + next: (value) => results.push(value), + error: (e) => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [1, 2, 3, error]); +}, "inspect(): Throwing an error in the observer complete handler is caught " + + "and sent to the error callback of the result observable"); + +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + }); + + const error = new Error("error from inspect() next handler"); + const result = source.inspect({ + next: (value) => { + if (value === 2) { + throw error; + } + }, + }); + + const results = []; + result.subscribe({ + next: (value) => results.push(value), + error: (e) => results.push(e), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, [1, error]); +}, "inspect(): Throwing an error in the next handler function in do should " + + "be caught and sent to the error callback of the result observable"); + +test(() => { + const source = new Observable(subscriber => {}); + + const result = source.inspect({ + subscribe: () => { + throw new Error("error from do subscribe handler"); + }, + }); + + const results = []; + result.subscribe({ + next: () => results.push("next"), + error: (e) => results.push(e.message), + complete: () => results.push("complete"), + }); + + assert_array_equals(results, ["error from do subscribe handler"]); +}, "inspect(): Errors thrown in subscribe() Inspector handler subscribe " + + "handler are caught and sent to error callback"); + +test(() => { + const results = []; + let sourceTeardownCall = 0; + const source = new Observable(subscriber => { + subscriber.addTeardown(() => { + sourceTeardownCall++; + results.push(`source teardown ${sourceTeardownCall}`); + }); + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.complete(); + }); + + let doUnsubscribeCall = 0; + const result = source.inspect({ + abort: (reason) => { + doUnsubscribeCall++; + results.push(`inspect() abort ${doUnsubscribeCall} ${reason}`); + }, + next: (value) => results.push(`inspect() next ${value}`), + error: (e) => results.push(`inspect() error ${e.message}`), + complete: () => results.push(`inspect() complete`), + }); + + const controller = new AbortController(); + result.subscribe({ + next: (value) => { + results.push(`result next ${value}`); + if (value === 2) { + controller.abort("abort reason"); + } + }, + error: (e) => results.push(`result error ${e.message}`), + complete: () => results.push(`result complete`), + }, { signal: controller.signal }); + + assert_array_equals(results, [ + "inspect() next 1", + "result next 1", + "inspect() next 2", + "result next 2", + "inspect() abort 1 abort reason", + "source teardown 1", + ]); +}, "inspect(): Provides a way to tap into the moment a source observable is unsubscribed from"); + +test(() => { + const results = []; + let sourceTeardownCall = 0; + const source = new Observable(subscriber => { + subscriber.addTeardown(() => { + sourceTeardownCall++; + results.push(`source teardown ${sourceTeardownCall}`); + }); + subscriber.next(1); + subscriber.next(2); + subscriber.next(3); + subscriber.complete(); + }); + + let inspectUnsubscribeCall = 0; + const result = source.inspect({ + next: (value) => results.push(`inspect() next ${value}`), + complete: () => results.push(`inspect() complete`), + abort: (reason) => { + inspectUnsubscribeCall++; + results.push(`inspect() abort ${inspectUnsubscribeCall} ${reason}`); + }, + }); + + result.subscribe({ + next: (value) => results.push(`result next ${value}`), + complete: () => results.push(`result complete`), + }); + + assert_array_equals(results, [ + "inspect() next 1", + "result next 1", + "inspect() next 2", + "result next 2", + "inspect() next 3", + "result next 3", + "source teardown 1", + "inspect() complete", + "result complete", + ]); +}, "inspect(): Inspector abort() handler is not called if the source " + + "completes before the result is unsubscribed from"); + +test(() => { + const source = new Observable(subscriber => { + subscriber.next(1); + }); + + const results = []; + + const result = source.inspect({ + abort: () => { + results.push('abort() handler run'); + throw new Error("error from inspect() subscribe handler"); + }, + }); + + const controller = new AbortController(); + + self.on('error').take(1).subscribe(e => + results.push(e.message + ', from report exception')); + + result.subscribe({ + next: (value) => { + results.push(value); + controller.abort(); + }, + // This should not be invoked at all!! + error: (e) => results.push(e.message + ', from Observer#error()'), + complete: () => results.push("complete"), + }, {signal: controller.signal}); + + assert_array_equals(results, [1, "abort() handler run", + "Uncaught Error: error from inspect() subscribe handler, from report " + + "exception"]); +}, "inspect(): Errors thrown from inspect()'s abort() handler are caught " + + "and reported to the global, because the subscription is already closed " + + "by the time the handler runs"); diff --git a/tests/wpt/tests/dom/observable/tentative/observable-some.any.js b/tests/wpt/tests/dom/observable/tentative/observable-some.any.js new file mode 100644 index 00000000000..b692610df32 --- /dev/null +++ b/tests/wpt/tests/dom/observable/tentative/observable-some.any.js @@ -0,0 +1,96 @@ +promise_test(async () => { + let inactiveAfterFirstGood = true; + + const source = new Observable(subscriber => { + subscriber.next("good"); + inactiveAfterFirstGood = !subscriber.active; + subscriber.next("good"); + subscriber.next("good"); + subscriber.complete(); + }); + + const result = await source.some((value) => value === "good"); + + assert_true(result, "Promise resolves with true if any value passes the predicate"); + + assert_true(inactiveAfterFirstGood, + "subscriber is inactive after the first value that passes the " + + "predicate, because the source was unsubscribed from"); +}, "some(): subscriber is inactive after the first value that passes the predicate, because the source was unsubscribed from"); + +promise_test(async () => { + const source = new Observable(subscriber => { + subscriber.next("bad"); + subscriber.next("bad"); + subscriber.next("bad"); + subscriber.complete(); + }); + + const result = await source.some((value) => value === "good"); + + assert_false(result, "some(): Promise resolves with false if no value passes the predicate"); +}); + +promise_test(async () => { + const source = new Observable(subscriber => { + subscriber.next("bad"); + subscriber.next("bad"); + subscriber.next("good"); + subscriber.complete(); + }); + + const result = await source.some((value) => value === "good"); + + assert_true(result, "some(): Promise resolves with true if any value passes the predicate"); +}); + +promise_test(async t => { + const source = new Observable(subscriber => { + subscriber.next("not used"); + }); + + const error = new Error("thrown from predicate"); + promise_rejects_exactly(t, error, source.some(() => {throw error}), + "The returned promise rejects with an error if the predicate errors"); +}, "some(): The returned promise rejects with an error if the predicate errors"); + +promise_test(async t => { + const error = new Error("error from source"); + const source = new Observable(subscriber => { + subscriber.error(error); + }); + + promise_rejects_exactly(t, error, source.some(() => true), + "The returned promise rejects with an error if the source observable errors"); +}, "some(): The returned promise rejects with an error if the source observable errors"); + +promise_test(async () => { + const source = new Observable(subscriber => { + subscriber.complete(); + }); + + const result = await source.some(() => true); + + assert_false(result, + "The returned promise resolves as false if the source observable " + + "completes without emitting a value"); +}, "some(): The returned promise resolves as false if the source observable " + + "completes without emitting a value"); + +promise_test(async t => { + let teardownCalled = false; + const source = new Observable(subscriber => { + subscriber.addTeardown(() => { + teardownCalled = true; + }); + }); + + const controller = new AbortController(); + const promise = source.some(() => true, { signal: controller.signal }); + + controller.abort(); + + promise_rejects_dom(t, 'AbortError', promise); + assert_true(teardownCalled, + "The teardown function is called when the signal is aborted"); +}, "some(): The return promise rejects with a DOMException if the signal is aborted"); diff --git a/tests/wpt/tests/dom/xslt/resources/xml2html.xsl b/tests/wpt/tests/dom/xslt/resources/xml2html.xsl index 07b967500ff..88b74a9620f 100644 --- a/tests/wpt/tests/dom/xslt/resources/xml2html.xsl +++ b/tests/wpt/tests/dom/xslt/resources/xml2html.xsl @@ -5,7 +5,7 @@ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:template match="/"> <html> <body> - <h2>My CD Collection</h2> + <h2 style="margin-top:0">My CD Collection</h2> <table border="1"> <tr bgcolor="#9acd32"> <th>Title</th> diff --git a/tests/wpt/tests/editing/data/delete.js b/tests/wpt/tests/editing/data/delete.js index 131c99b1d5a..c4d1225ef35 100644 --- a/tests/wpt/tests/editing/data/delete.js +++ b/tests/wpt/tests/editing/data/delete.js @@ -2044,12 +2044,12 @@ var browserTests = [ {"defaultparagraphseparator":[false,false,"div",false,false,"p"],"delete":[false,false,"",false,false,""]}], ["foo<br><br>{<p>]bar</p>", [["defaultparagraphseparator","div"],["delete",""]], - "foo<br>{}bar", + "foo<br><p>bar</p>", [true,true], {"defaultparagraphseparator":[false,false,"p",false,false,"div"],"delete":[false,false,"",false,false,""]}], ["foo<br><br>{<p>]bar</p>", [["defaultparagraphseparator","p"],["delete",""]], - "foo<br>{}bar", + "foo<br><p>bar</p>", [true,true], {"defaultparagraphseparator":[false,false,"div",false,false,"p"],"delete":[false,false,"",false,false,""]}], ["<p>foo<br>{</p><p>}bar</p>", @@ -2359,7 +2359,7 @@ var browserTests = [ {"delete":[false,false,"",false,false,""]}], ["<ol><li>foo</ol>[bar<ol><li>]baz</ol>", [["delete",""]], - "<ol><li>foo</li></ol>{}baz", + "<ol><li>foo</li></ol><ol><li>baz</li></ol>", [true], {"delete":[false,false,"",false,false,""]}], ["<ol><li>foo</ol><p>[bar<ol><li>]baz</ol>", diff --git a/tests/wpt/tests/editing/data/forwarddelete.js b/tests/wpt/tests/editing/data/forwarddelete.js index ea590a4fbba..a881fb6ccf4 100644 --- a/tests/wpt/tests/editing/data/forwarddelete.js +++ b/tests/wpt/tests/editing/data/forwarddelete.js @@ -2009,12 +2009,12 @@ var browserTests = [ {"defaultparagraphseparator":[false,false,"div",false,false,"p"],"forwarddelete":[false,false,"",false,false,""]}], ["foo<br><br>{<p>]bar</p>", [["defaultparagraphseparator","div"],["forwarddelete",""]], - "foo<br>{}bar", + "foo<br><p>bar</p>", [true,true], {"defaultparagraphseparator":[false,false,"p",false,false,"div"],"forwarddelete":[false,false,"",false,false,""]}], ["foo<br><br>{<p>]bar</p>", [["defaultparagraphseparator","p"],["forwarddelete",""]], - "foo<br>{}bar", + "foo<br><p>bar</p>", [true,true], {"defaultparagraphseparator":[false,false,"div",false,false,"p"],"forwarddelete":[false,false,"",false,false,""]}], ["<p>foo<br>{</p><p>}bar</p>", @@ -2184,7 +2184,7 @@ var browserTests = [ {"forwarddelete":[false,false,"",false,false,""]}], ["<ol><li>foo</ol>{}<br><ol><li>bar</ol>", [["forwarddelete",""]], - "<ol><li>foo</li></ol>{}bar", + "<ol><li>foo</li></ol><ol><li>bar</li></ol>", [true], {"forwarddelete":[false,false,"",false,false,""]}], ["<ol><li>foo</ol><p>{}<br></p><ol><li>bar</ol>", @@ -2199,22 +2199,22 @@ var browserTests = [ {"forwarddelete":[false,false,"",false,false,""]}], ["<ol id=a><li>foo</ol>{}<br><ol><li>bar</ol>", [["forwarddelete",""]], - "<ol id=\"a\"><li>foo</li></ol>{}bar", + "<ol id=\"a\"><li>foo</li></ol><ol><li>bar</li></ol>", [true], {"forwarddelete":[false,false,"",false,false,""]}], ["<ol><li>foo</ol>{}<br><ol id=b><li>bar</ol>", [["forwarddelete",""]], - "<ol><li>foo</li></ol>{}bar", + "<ol><li>foo</li></ol><ol id=\"b\"><li>bar</li></ol>", [true], {"forwarddelete":[false,false,"",false,false,""]}], ["<ol id=a><li>foo</ol>{}<br><ol id=b><li>bar</ol>", [["forwarddelete",""]], - "<ol id=\"a\"><li>foo</li></ol>{}bar", + "<ol id=\"a\"><li>foo</li></ol><ol id=\"b\"><li>bar</li></ol>", [true], {"forwarddelete":[false,false,"",false,false,""]}], ["<ol class=a><li>foo</ol>{}<br><ol class=b><li>bar</ol>", [["forwarddelete",""]], - "<ol class=\"a\"><li>foo</li></ol>{}bar", + "<ol class=\"a\"><li>foo</li></ol><ol class=\"b\"><li>bar</li></ol>", [true], {"forwarddelete":[false,false,"",false,false,""]}], ["<ol><ol><li>foo</ol><li>{}<br><ol><li>bar</ol></ol>", @@ -2259,7 +2259,7 @@ var browserTests = [ {"forwarddelete":[false,false,"",false,false,""]}], ["<ol><li>foo</ol>[bar<ol><li>]baz</ol>", [["forwarddelete",""]], - "<ol><li>foo</li></ol>{}baz", + "<ol><li>foo</li></ol><ol><li>baz</li></ol>", [true], {"forwarddelete":[false,false,"",false,false,""]}], ["<ol><li>foo</ol><p>[bar<ol><li>]baz</ol>", @@ -2269,12 +2269,12 @@ var browserTests = [ {"forwarddelete":[false,false,"",false,false,""]}], ["<ol><li>foo</ol><p>[bar<ol><li><p>]baz</ol>", [["defaultparagraphseparator","div"],["forwarddelete",""]], - "<ol><li>foo</li></ol><p>{}baz</p>", + "<ol><li>foo</li></ol><ol><li><p>{}baz</p></li></ol>", [true,true], {"defaultparagraphseparator":[false,false,"p",false,false,"div"],"forwarddelete":[false,false,"",false,false,""]}], ["<ol><li>foo</ol><p>[bar<ol><li><p>]baz</ol>", [["defaultparagraphseparator","p"],["forwarddelete",""]], - "<ol><li>foo</li></ol><p>{}baz</p>", + "<ol><li>foo</li></ol><ol><li><p>{}baz</p></li></ol>", [true,true], {"defaultparagraphseparator":[false,false,"div",false,false,"p"],"forwarddelete":[false,false,"",false,false,""]}], ["<ol><li>foo</ol><ol><li>[]bar</ol>", @@ -2289,7 +2289,7 @@ var browserTests = [ {"forwarddelete":[false,false,"",false,false,""]}], ["<ul><li>foo</ul>{}<br><ul><li>bar</ul>", [["forwarddelete",""]], - "<ul><li>foo</li></ul>{}bar", + "<ul><li>foo</li></ul><ul><li>bar</li></ul>", [true], {"forwarddelete":[false,false,"",false,false,""]}], ["<ul><li>foo</ul><p>{}<br></p><ul><li>bar</ul>", @@ -2304,7 +2304,7 @@ var browserTests = [ {"forwarddelete":[false,false,"",false,false,""]}], ["<ol><li>foo</ol>{}<br><ul><li>bar</ul>", [["forwarddelete",""]], - "<ol><li>foo</li></ol>{}bar", + "<ol><li>foo</li></ol><ul><li>bar</li></ul>", [true], {"forwarddelete":[false,false,"",false,false,""]}], ["<ol><li>foo</ol><p>{}<br></p><ul><li>bar</ul>", @@ -2314,7 +2314,7 @@ var browserTests = [ {"forwarddelete":[false,false,"",false,false,""]}], ["<ul><li>foo</ul>{}<br><ol><li>bar</ol>", [["forwarddelete",""]], - "<ul><li>foo</li></ul>{}bar", + "<ul><li>foo</li></ul><ol><li>bar</li></ol>", [true], {"forwarddelete":[false,false,"",false,false,""]}], ["<ul><li>foo</ul><p>{}<br></p><ol><li>bar</ol>", diff --git a/tests/wpt/tests/editing/include/editor-test-utils.js b/tests/wpt/tests/editing/include/editor-test-utils.js index d0d50d22a66..b180f3343fd 100644 --- a/tests/wpt/tests/editing/include/editor-test-utils.js +++ b/tests/wpt/tests/editing/include/editor-test-utils.js @@ -424,4 +424,79 @@ class EditorTestUtils { ); } } + + static getRangeArrayDescription(arrayOfRanges) { + if (arrayOfRanges === null) { + return "null"; + } + if (arrayOfRanges === undefined) { + return "undefined"; + } + if (!Array.isArray(arrayOfRanges)) { + return "Unknown Object"; + } + if (arrayOfRanges.length === 0) { + return "[]"; + } + let result = ""; + for (let range of arrayOfRanges) { + if (result === "") { + result = "["; + } else { + result += ","; + } + result += `{${EditorTestUtils.getRangeDescription(range)}}`; + } + result += "]"; + return result; + } + + static getNodeDescription(node) { + if (!node) { + return "null"; + } + switch (node.nodeType) { + case Node.TEXT_NODE: + case Node.COMMENT_NODE: + case Node.CDATA_SECTION_NODE: + return `${node.nodeName} "${node.data.replaceAll("\n", "\\\\n")}"`; + case Node.ELEMENT_NODE: + return `<${node.nodeName.toLowerCase()}${ + node.hasAttribute("id") ? ` id="${node.getAttribute("id")}"` : "" + }${ + node.hasAttribute("class") ? ` class="${node.getAttribute("class")}"` : "" + }${ + node.hasAttribute("contenteditable") + ? ` contenteditable="${node.getAttribute("contenteditable")}"` + : "" + }${ + node.inert ? ` inert` : "" + }${ + node.hidden ? ` hidden` : "" + }${ + node.readonly ? ` readonly` : "" + }${ + node.disabled ? ` disabled` : "" + }>`; + default: + return `${node.nodeName}`; + } + } + + static getRangeDescription(range) { + if (range === null) { + return "null"; + } + if (range === undefined) { + return "undefined"; + } + return range.startContainer == range.endContainer && + range.startOffset == range.endOffset + ? `(${EditorTestUtils.getNodeDescription(range.startContainer)}, ${range.startOffset})` + : `(${EditorTestUtils.getNodeDescription(range.startContainer)}, ${ + range.startOffset + }) - (${EditorTestUtils.getNodeDescription(range.endContainer)}, ${range.endOffset})`; + } + + } diff --git a/tests/wpt/tests/editing/other/delete-without-unwrapping-first-line-of-child-block.html b/tests/wpt/tests/editing/other/delete-without-unwrapping-first-line-of-child-block.html new file mode 100644 index 00000000000..99f8f058888 --- /dev/null +++ b/tests/wpt/tests/editing/other/delete-without-unwrapping-first-line-of-child-block.html @@ -0,0 +1,1020 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<meta name="timeout" content="long"> +<meta name="variant" content="?method=BackspaceKey&lineBreak=br"> +<meta name="variant" content="?method=DeleteKey&lineBreak=br"> +<meta name="variant" content="?method=deleteCommand&lineBreak=br"> +<meta name="variant" content="?method=forwardDeleteCommand&lineBreak=br"> +<meta name="variant" content="?method=BackspaceKey&lineBreak=preformat"> +<meta name="variant" content="?method=DeleteKey&lineBreak=preformat"> +<meta name="variant" content="?method=deleteCommand&lineBreak=preformat"> +<meta name="variant" content="?method=forwardDeleteCommand&lineBreak=preformat"> +<title>Tests for deleting preceding lines of right child block if range ends at start of the right child</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src="../include/editor-test-utils.js"></script> +<script> +"use strict"; + +/** + * Browsers delete only preceding lines (and selected content in the child + * block) when the deleting range starts from a line and ends in a child block + * without unwrapping the (new) first line of the child block at end. Note that + * this is a special handling for the above case, i.e., if the range starts from + * a middle of a preceding line of the child block, the first line of the child + * block should be unwrapped and merged into the preceding line. This is also + * applied when the range is directly replaced with new content like typing a + * character. Finally, selection should be collapsed at start of the child + * block and new content should be inserted at start of the child block. + * + * This file also tests getTargetRanges() of `beforeinput` of at deletion and + * replacing the selection directly. In the former case, if the range ends at + * start of the child block, browsers do not touch the child block. Therefore, + * the target ranges should the a range deleting the preceding lines, i.e., + * should be end at the child block. When the range is replaced directly, the + * content will be inserted at start of the child block, and also when the range + * selects some content in the child block, browsers touch the child block. + * Therefore, the target range should end at the next insertion point. + */ + +const searchParams = new URLSearchParams(document.location.search); +const testUserInput = searchParams.get("method") == "BackspaceKey" || searchParams.get("method") == "DeleteKey"; +const testBackward = searchParams.get("method") == "BackspaceKey" || searchParams.get("method") == "deleteCommand"; +const deleteMethod = + testUserInput + ? testBackward ? "Backspace" : "Delete" + : `document.execCommand("${testBackward ? "delete" : "forwarddelete"}")`; +const insertTextMethod = testUserInput ? "Typing \"X\"" : "document.execCommand(\"insertText\", false, \"X\")"; +const lineBreak = searchParams.get("lineBreak") == "br" ? "<br>" : "\n"; +const lineBreakIsBR = lineBreak == "<br>"; + +function run(editorUtils) { + if (testUserInput) { + return testBackward ? editorUtils.sendBackspaceKey() : editorUtils.sendDeleteKey(); + } + editorUtils.document.execCommand(testBackward ? "delete" : "forwardDelete"); +} + +function typeCharacter(editorUtils, ch) { + if (testUserInput) { + return editorUtils.sendKey(ch); + } + document.execCommand("insertText", false, ch); +} + +async function runDeleteTest( + runningTest, + testUtils, + initialInnerHTML, + expectedAfterDeletion, + whatShouldHappenAfterDeletion, + expectedAfterDeletionAndInsertion, + whatShouldHappenAfterDeletionAndInsertion, + expectedTargetRangesAtDeletion, + whatGetTargetRangesShouldReturn +) { + let targetRanges = []; + if (testUserInput) { + testUtils.editingHost.addEventListener( + "beforeinput", + event => targetRanges = event.getTargetRanges(), + {once: true} + ); + } + await run(testUtils); + (Array.isArray(expectedAfterDeletion) ? assert_in_array : assert_equals)( + testUtils.editingHost.innerHTML, + expectedAfterDeletion, + `${runningTest.name} ${whatShouldHappenAfterDeletion}` + ); + if (testUserInput) { + test(() => { + const arrayOfStringifiedExpectedTargetRanges = (() => { + let arrayOfTargetRanges = []; + for (const expectedTargetRanges of expectedTargetRangesAtDeletion) { + arrayOfTargetRanges.push( + EditorTestUtils.getRangeArrayDescription(expectedTargetRanges) + ); + } + return arrayOfTargetRanges; + })(); + assert_in_array( + EditorTestUtils.getRangeArrayDescription(targetRanges), + arrayOfStringifiedExpectedTargetRanges + ); + }, `getTargetRanges() for ${runningTest.name} ${whatGetTargetRangesShouldReturn}`); + } + await typeCharacter(testUtils, "X"); + (Array.isArray(expectedAfterDeletionAndInsertion) ? assert_in_array : assert_equals)( + testUtils.editingHost.innerHTML, + expectedAfterDeletionAndInsertion, + `${insertTextMethod} after ${runningTest.name} ${whatShouldHappenAfterDeletionAndInsertion}` + ); +} + +async function runReplacingTest( + runningTest, + testUtils, + initialInnerHTML, + expectedAfterReplacing, + whatShouldHappenAfterReplacing, + expectedTargetRangesAtReplace, + whatGetTargetRangesShouldReturn +) { + let targetRanges = []; + if (testUserInput) { + testUtils.editingHost.addEventListener( + "beforeinput", + event => targetRanges = event.getTargetRanges(), + {once: true} + ); + } + await typeCharacter(testUtils, "X"); + (Array.isArray(expectedAfterReplacing) ? assert_in_array : assert_equals)( + testUtils.editingHost.innerHTML, + expectedAfterReplacing, + `${runningTest.name} ${whatShouldHappenAfterReplacing}` + ); + if (testUserInput) { + test(() => { + const arrayOfStringifiedExpectedTargetRanges = (() => { + let arrayOfTargetRanges = []; + for (const expectedTargetRanges of expectedTargetRangesAtReplace) { + arrayOfTargetRanges.push( + EditorTestUtils.getRangeArrayDescription(expectedTargetRanges) + ); + } + return arrayOfTargetRanges; + })(); + assert_in_array( + EditorTestUtils.getRangeArrayDescription(targetRanges), + arrayOfStringifiedExpectedTargetRanges + ); + }, `getTargetRanges() for ${runningTest.name} ${whatGetTargetRangesShouldReturn}`); + } +} + +addEventListener("load", () => { + const editingHost = document.querySelector("div[contenteditable]"); + const selStart = lineBreakIsBR ? "{" : "["; + const selCollapsed = lineBreakIsBR ? "{}" : "[]"; + editingHost.style.whiteSpace = lineBreakIsBR ? "normal" : "pre"; + const testUtils = new EditorTestUtils(editingHost); + (() => { + const initialInnerHTML = + `abc${lineBreak}${selStart}${lineBreak}<div id="child">]def<br>ghi</div>`; + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + await runDeleteTest( + t, testUtils, initialInnerHTML, + [ + `abc${lineBreak}<div id="child">def<br>ghi</div>`, + `abc<div id="child">def<br>ghi</div>`, + ], + "should delete only the preceding empty line of the child <div>", + [ + `abc${lineBreak}<div id="child">Xdef<br>ghi</div>`, + `abc<div id="child">Xdef<br>ghi</div>`, + ], + "should insert text into the child <div>", + lineBreakIsBR + ? [ + // abc<br>{<br>}<div> + [{ startContainer: editingHost, startOffset: 2, endContainer: editingHost, endOffset: 3 }], + // abc{<br><br>}<div> + [{ startContainer: editingHost, startOffset: 1, endContainer: editingHost, endOffset: 3 }], + // abc[<br><br>}<div> + [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: editingHost, endOffset: 3 }], + ] + : [ + // abc\n[\n}<div> + [{ startContainer: editingHost.firstChild, startOffset: "abc\n".length, endContainer: editingHost, endOffset: 1 }], + // abc[\n\n}<div> + [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: editingHost, endOffset: 1 }], + // abc\n[\n]<div> + [{ startContainer: editingHost.firstChild, startOffset: "abc\n".length, endContainer: editingHost.firstChild, endOffset: "abc\n\n".length }], + // abc[\n\n]<div> + [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: editingHost.firstChild, endOffset: "abc\n\n".length }], + ], + "should return a range before the child <div>" + ); + }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`); + + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + const firstTextInChildDiv = editingHost.querySelector("div").firstChild; + await runReplacingTest( + t, testUtils, initialInnerHTML, + [ + `abc${lineBreak}<div id="child">Xdef<br>ghi</div>`, + `abc<div id="child">Xdef<br>ghi</div>`, + ], + "should not unwrap the first line of the child <div>", + lineBreakIsBR + ? [ + // abc<br>{<br><div>]def + [{ startContainer: editingHost, startOffset: 2, endContainer: firstTextInChildDiv, endOffset: 0 }], + // abc{<br><br><div>]def + [{ startContainer: editingHost, startOffset: 1, endContainer: firstTextInChildDiv, endOffset: 0 }], + // abc[<br><br><div>]def + [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: firstTextInChildDiv, endOffset: 0 }], + ] + : [ + // abc\n[\n<div>]def + [{ startContainer: editingHost.firstChild, startOffset: "abc\n".length, endContainer: firstTextInChildDiv, endOffset: 0 }], + // abc[\n\n<div>]def + [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: firstTextInChildDiv, endOffset: 0 }], + ], + "should return a range ending in the child <div>" + ); + }, `${insertTextMethod} at ${initialInnerHTML.replaceAll("\n", "\\n")}`); + })(); + + (() => { + const initialInnerHTML = + `${lineBreak}[abc${lineBreak}<div id="child">]def<br>ghi</div>`; + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + await runDeleteTest( + t, testUtils, initialInnerHTML, + `${lineBreak}<div id="child">def<br>ghi</div>`, + "should delete only the preceding empty line of the child <div>", + `${lineBreak}<div id="child">Xdef<br>ghi</div>`, + "should insert text into the child <div>", + lineBreakIsBR + ? [ + // <br>[abc<br>}<div> + [{ startContainer: editingHost.firstChild.nextSibling, startOffset: 0, endContainer: editingHost, endOffset: 3 }], + ] + : [ + // \n[abc\n}<div> + [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: editingHost, endOffset: 1 }], + // \n[abc\n]<div> + [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: editingHost.firstChild, endOffset: "\nabc\n".length }], + ], + "should return a range before the child <div>" + ); + }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`); + + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + const firstTextInChildDiv = editingHost.querySelector("div").firstChild; + await runReplacingTest( + t, testUtils, initialInnerHTML, + `${lineBreak}<div id="child">Xdef<br>ghi</div>`, + "should not unwrap the first line of the child <div>", + lineBreakIsBR + ? [ + // <br>[abc<br><div>]def + [{ startContainer: editingHost.firstChild.nextSibling, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 0 }], + ] + : [ + // \n[abc\n<div>]def + [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: firstTextInChildDiv, endOffset: 0 }], + ], + "should return a range ending in the child <div>" + ); + }, `${insertTextMethod} at ${initialInnerHTML.replaceAll("\n", "\\n")}`); + })(); + + (() => { + const initialInnerHTML = + `${lineBreak}${selStart}${lineBreak}<div id="child">]def<br>ghi</div>`; + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + await runDeleteTest( + t, testUtils, initialInnerHTML, + `${lineBreak}<div id="child">def<br>ghi</div>`, + "should delete only the preceding empty line of the child <div>", + `${lineBreak}<div id="child">Xdef<br>ghi</div>`, + "should insert text into the child <div>", + lineBreakIsBR + ? [ + // <br>{<br>}<div> + [{ startContainer: editingHost, startOffset: 1, endContainer: editingHost, endOffset: 2 }], + ] + : [ + // \n[\n}<div> + [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: editingHost, endOffset: 1 }], + // \n[\n]<div> + [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: editingHost.firstChild, endOffset: "\n\n".length }], + ], + "should return a range before the child <div>" + ); + }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`); + + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + const firstTextInChildDiv = editingHost.querySelector("div").firstChild; + await runReplacingTest( + t, testUtils, initialInnerHTML, + `${lineBreak}<div id="child">Xdef<br>ghi</div>`, + "should not unwrap the first line of the child <div>", + lineBreakIsBR + ? [ + // <br>{<br><div>]def + [{ startContainer: editingHost, startOffset: 1, endContainer: firstTextInChildDiv, endOffset: 0 }], + ] + : [ + // \n[\n<div>]def + [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: firstTextInChildDiv, endOffset: 0 }], + ], + "should return a range ending in the child <div>" + ); + }, `${insertTextMethod} at ${initialInnerHTML.replaceAll("\n", "\\n")}`); + })(); + + (() => { + const initialInnerHTML = + `${selStart}${lineBreak}${lineBreak}<div id="child">]def<br>ghi</div>`; + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + await runDeleteTest( + t, testUtils, initialInnerHTML, + `<div id="child">def<br>ghi</div>`, + "should delete only the preceding empty line of the child <div>", + `<div id="child">Xdef<br>ghi</div>`, + "should insert text into the child <div>", + lineBreakIsBR + ? [ + // {<br><br>}<div> + [{ startContainer: editingHost, startOffset: 0, endContainer: editingHost, endOffset: 2 }], + ] + : [ + // [\n\n}<div> + [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: editingHost, endOffset: 1 }], + // [\n\n}<div> + [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: editingHost.firstChild, endOffset: "\n\n".length }], + ], + "should return a range before the child <div>" + ); + }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`); + + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + const firstTextInChildDiv = editingHost.querySelector("div").firstChild; + await runReplacingTest( + t, testUtils, initialInnerHTML, + `<div id="child">Xdef<br>ghi</div>`, + "should not unwrap the first line of the child <div>", + lineBreakIsBR + ? [ + // {<br><br><div>]def + [{ startContainer: editingHost, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 0 }], + ] + : [ + // [\n\n<div>]def + [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 0 }], + ], + "should return a range ending in the child <div>" + ); + }, `${insertTextMethod} at ${initialInnerHTML.replaceAll("\n", "\\n")}`); + })(); + + (() => { + const initialInnerHTML = + `[abc${lineBreak}${lineBreak}<div id="child">]def<br>ghi</div>`; + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + await runDeleteTest( + t, testUtils, initialInnerHTML, + `<div id="child">def<br>ghi</div>`, + "should delete only the preceding empty line of the child <div>", + `<div id="child">Xdef<br>ghi</div>`, + "should insert text into the child <div>", + lineBreakIsBR + ? [ + // [abc<br><br>}<div> + [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: editingHost, endOffset: 3 }], + ] + : [ + // {abc\n\n}<div> + [{ startContainer: editingHost, startOffset: 0, endContainer: editingHost, endOffset: 1 }], + // [abc\n\n}<div> + [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: editingHost, endOffset: 1 }], + // [abc\n\n}<div> + [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: editingHost.firstChild, endOffset: "abc\n\n".length }], + ], + "should return a range before the child <div>" + ); + }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`); + + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + const firstTextInChildDiv = editingHost.querySelector("div").firstChild; + await runReplacingTest( + t, testUtils, initialInnerHTML, + `<div id="child">Xdef<br>ghi</div>`, + "should not unwrap the first line of the child <div>", + lineBreakIsBR + ? [ + // [abc<br><div>]def + [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 0 }], + ] + : [ + // [abc\n<div>]def + [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 0 }], + ], + "should return a range ending in the child <div>" + ); + }, `${insertTextMethod} at ${initialInnerHTML.replaceAll("\n", "\\n")}`); + })(); + + (() => { + const initialInnerHTML = + `abc${lineBreak}${selStart}${lineBreak}<div id="child">d]ef<br>ghi</div>`; + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + const firstTextInChildDiv = editingHost.querySelector("div").firstChild; + await runDeleteTest( + t, testUtils, initialInnerHTML, + [ + `abc${lineBreak}<div id="child">ef<br>ghi</div>`, + `abc<div id="child">ef<br>ghi</div>`, + ], + "should delete only the preceding empty line of the child <div> and selected text in the <div>", + [ + `abc${lineBreak}<div id="child">Xef<br>ghi</div>`, + `abc<div id="child">Xef<br>ghi</div>`, + ], + "should insert text into the child <div>", + lineBreakIsBR + ? [ + // abc<br>{<br><div>d]ef + [{ startContainer: editingHost, startOffset: 2, endContainer: firstTextInChildDiv, endOffset: 1 }], + // abc{<br><br><div>d]ef + [{ startContainer: editingHost, startOffset: 1, endContainer: firstTextInChildDiv, endOffset: 1 }], + // abc[<br><br><div>d]ef + [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: firstTextInChildDiv, endOffset: 1 }], + ] + : [ + // abc\n[\n}<div>d]ef + [{ startContainer: editingHost.firstChild, startOffset: "abc\n".length, endContainer: firstTextInChildDiv, endOffset: 1 }], + // abc[\n\n}<div>d]ef + [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: firstTextInChildDiv, endOffset: 1 }], + ], + "should return a range ends at start of the child <div>" + ); + }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`); + + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + const firstTextInChildDiv = editingHost.querySelector("div").firstChild; + await runReplacingTest( + t, testUtils, initialInnerHTML, + [ + `abc${lineBreak}<div id="child">Xef<br>ghi</div>`, + `abc<div id="child">Xef<br>ghi</div>`, + ], + "should not unwrap the first line of the child <div>", + lineBreakIsBR + ? [ + // abc<br>{<br><div>d]ef + [{ startContainer: editingHost, startOffset: 2, endContainer: firstTextInChildDiv, endOffset: 1 }], + // abc{<br><br><div>d]ef + [{ startContainer: editingHost, startOffset: 1, endContainer: firstTextInChildDiv, endOffset: 1 }], + // abc[<br><br><div>d]ef + [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: firstTextInChildDiv, endOffset: 1 }], + ] + : [ + // abc\n[\n<div>d]ef + [{ startContainer: editingHost.firstChild, startOffset: "abc\n".length, endContainer: firstTextInChildDiv, endOffset: 1 }], + // abc[\n\n<div>d]ef + [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: firstTextInChildDiv, endOffset: 1 }], + ], + "should return a range ends at start of the child <div>" + ); + }, `${insertTextMethod} at ${initialInnerHTML.replaceAll("\n", "\\n")}`); + })(); + + (() => { + const initialInnerHTML = + `${lineBreak}[abc${lineBreak}<div id="child">d]ef<br>ghi</div>`; + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + const firstTextInChildDiv = editingHost.querySelector("div").firstChild; + await runDeleteTest( + t, testUtils, initialInnerHTML, + `${lineBreak}<div id="child">ef<br>ghi</div>`, + "should delete only the preceding empty line of the child <div> and the selected content in the <div>", + `${lineBreak}<div id="child">Xef<br>ghi</div>`, + "should insert text into the child <div>", + lineBreakIsBR + ? [ + // <br>[abc<br><div>d]ef + [{ startContainer: editingHost.firstChild.nextSibling, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 1 }], + ] + : [ + // \n[abc\n<div>d]ef + [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: firstTextInChildDiv, endOffset: 1 }], + ], + "should return a range ends at start of the child <div>" + ); + }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`); + + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + const firstTextInChildDiv = editingHost.querySelector("div").firstChild; + await runReplacingTest( + t, testUtils, initialInnerHTML, + `${lineBreak}<div id="child">Xef<br>ghi</div>`, + "should not unwrap the first line of the child <div>", + lineBreakIsBR + ? [ + // <br>[abc<br><div>d]ef + [{ startContainer: editingHost.firstChild.nextSibling, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 1 }], + ] + : [ + // \n[abc\n<div>d]ef + [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: firstTextInChildDiv, endOffset: 1 }], + ], + "should return a range ends at start of the child <div>" + ); + }, `${insertTextMethod} at ${initialInnerHTML.replaceAll("\n", "\\n")}`); + })(); + + (() => { + const initialInnerHTML = + `${lineBreak}${selStart}${lineBreak}<div id="child">d]ef<br>ghi</div>`; + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + const firstTextInChildDiv = editingHost.querySelector("div").firstChild; + await runDeleteTest( + t, testUtils, initialInnerHTML, + `${lineBreak}<div id="child">ef<br>ghi</div>`, + "should delete only the preceding empty line of the child <div> and selected content in the <div>", + `${lineBreak}<div id="child">Xef<br>ghi</div>`, + "should insert text into the child <div>", + lineBreakIsBR + ? [ + // <br>{<br><div>d]ef + [{ startContainer: editingHost, startOffset: 1, endContainer: firstTextInChildDiv, endOffset: 1 }], + ] + : [ + // \n[\n<div>d]ef + [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: firstTextInChildDiv, endOffset: 1 }], + ], + "should return a range ends at start of the child <div>" + ); + }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`); + + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + const firstTextInChildDiv = editingHost.querySelector("div").firstChild; + await runReplacingTest( + t, testUtils, initialInnerHTML, + `${lineBreak}<div id="child">Xef<br>ghi</div>`, + "should not unwrap the first line of the child <div>", + lineBreakIsBR + ? [ + // <br>{<br><div>d]ef + [{ startContainer: editingHost, startOffset: 1, endContainer: firstTextInChildDiv, endOffset: 1 }], + ] + : [ + // \n[\n<div>d]ef + [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: firstTextInChildDiv, endOffset: 1 }], + ], + "should return a range ends at start of the child <div>" + ); + }, `${insertTextMethod} at ${initialInnerHTML.replaceAll("\n", "\\n")}`); + })(); + + (() => { + const initialInnerHTML = + `${selStart}${lineBreak}${lineBreak}<div id="child">d]ef<br>ghi</div>`; + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + const firstTextInChildDiv = editingHost.querySelector("div").firstChild; + await runDeleteTest( + t, testUtils, initialInnerHTML, + `<div id="child">ef<br>ghi</div>`, + "should delete only the preceding empty line of the child <div> and selected content in the <div>", + `<div id="child">Xef<br>ghi</div>`, + "should insert text into the child <div>", + lineBreakIsBR + ? [ + // {<br><br><div>d]ef + [{ startContainer: editingHost, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 1 }], + ] + : [ + // [\n\n<div>d]ef + [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 1 }], + ], + "should return a range ends at start of the child <div>" + ); + }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`); + + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + const firstTextInChildDiv = editingHost.querySelector("div").firstChild; + await runReplacingTest( + t, testUtils, initialInnerHTML, + `<div id="child">Xef<br>ghi</div>`, + "should not unwrap the first line of the child <div>", + lineBreakIsBR + ? [ + // {<br><br><div>d]ef + [{ startContainer: editingHost, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 1 }], + ] + : [ + // [\n\n<div>d]ef + [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 1 }], + ], + "should return a range ends at start of the child <div>" + ); + }, `${insertTextMethod} at ${initialInnerHTML.replaceAll("\n", "\\n")}`); + })(); + + (() => { + const initialInnerHTML = + `[abc${lineBreak}${lineBreak}<div id="child">d]ef<br>ghi</div>`; + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + const firstTextInChildDiv = editingHost.querySelector("div").firstChild; + await runDeleteTest( + t, testUtils, initialInnerHTML, + `<div id="child">ef<br>ghi</div>`, + "should delete only the preceding empty line of the child <div> and selected content in the <div>", + `<div id="child">Xef<br>ghi</div>`, + "should insert text into the child <div>", + lineBreakIsBR + ? [ + // [abc<br><br><div>d]ef + [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 1 }], + ] + : [ + // [abc\n\n}<div>d]ef + [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 1 }], + ], + "should return a range ends at start of the child <div>" + ); + }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`); + + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + const firstTextInChildDiv = editingHost.querySelector("div").firstChild; + await runReplacingTest( + t, testUtils, initialInnerHTML, + `<div id="child">Xef<br>ghi</div>`, + "should not unwrap the first line of the child <div>", + lineBreakIsBR + ? [ + // [abc<br><div>d]ef + [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 1 }], + ] + : [ + // [abc\n<div>d]ef + [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: firstTextInChildDiv, endOffset: 1 }], + ], + "should return a range ends at start of the child <div>" + ); + }, `${insertTextMethod} at ${initialInnerHTML.replaceAll("\n", "\\n")}`); + })(); + + (function test_BackspaceForCollapsedSelection() { + if (!testBackward) { + return; + } + (() => { + const initialInnerHTML = + `abc${lineBreak}${lineBreak}<div id="child">[]def<br>ghi</div>`; + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + await runDeleteTest( + t, testUtils, initialInnerHTML, + [ + `abc${lineBreak}<div id="child">def<br>ghi</div>`, + `abc<div id="child">def<br>ghi</div>`, + ], + "should delete only the preceding empty line of the child <div>", + [ + `abc${lineBreak}<div id="child">Xdef<br>ghi</div>`, + `abc<div id="child">Xdef<br>ghi</div>`, + ], + "should insert text into the child <div>", + lineBreakIsBR + ? [ + // abc<br>{<br>}<div> + [{ startContainer: editingHost, startOffset: 2, endContainer: editingHost, endOffset: 3 }], + // abc{<br><br>}<div> + [{ startContainer: editingHost, startOffset: 1, endContainer: editingHost, endOffset: 3 }], + // abc[<br><br>}<div> + [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: editingHost, endOffset: 3 }], + ] + : [ + // abc\n[\n}<div> + [{ startContainer: editingHost.firstChild, startOffset: "abc\n".length, endContainer: editingHost, endOffset: 1 }], + // abc[\n\n}<div> + [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: editingHost, endOffset: 1 }], + // abc\n[\n]<div> + [{ startContainer: editingHost.firstChild, startOffset: "abc\n".length, endContainer: editingHost.firstChild, endOffset: "abc\n\n".length }], + // abc[\n\n]<div> + [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: editingHost.firstChild, endOffset: "abc\n\n".length }], + ], + "should return a range before the child <div>" + ); + }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`); + })(); + + (() => { + const initialInnerHTML = + `${lineBreak}${lineBreak}<div id="child">[]def<br>ghi</div>`; + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + await runDeleteTest( + t, testUtils, initialInnerHTML, + `${lineBreak}<div id="child">def<br>ghi</div>`, + "should delete only the preceding empty line of the child <div>", + `${lineBreak}<div id="child">Xdef<br>ghi</div>`, + "should insert text into the child <div>", + lineBreakIsBR + ? [ + // <br>{<br>}<div> + [{ startContainer: editingHost, startOffset: 1, endContainer: editingHost, endOffset: 2 }], + ] + : [ + // \n[\n}<div> + [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: editingHost, endOffset: 1 }], + // \n[\n]<div> + [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: editingHost.firstChild, endOffset: "\n\n".length }], + ], + "should return a range before the child <div>" + ); + }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`); + })(); + + (() => { + const initialInnerHTML = + `${lineBreak}<div id="child">[]def<br>ghi</div>`; + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + await runDeleteTest( + t, testUtils, initialInnerHTML, + `<div id="child">def<br>ghi</div>`, + "should delete only the preceding empty line of the child <div>", + `<div id="child">Xdef<br>ghi</div>`, + "should insert text into the child <div>", + lineBreakIsBR + ? [ + // {<br>}<div> + [{ startContainer: editingHost, startOffset: 0, endContainer: editingHost, endOffset: 1 }], + ] + : [ + // {\n}<div> + [{ startContainer: editingHost, startOffset: 0, endContainer: editingHost, endOffset: 1 }], + // [\n}<div> + [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: editingHost, endOffset: 1 }], + // [\n]<div> + [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: editingHost.firstChild, endOffset: "\n".length }], + ], + "should return a range before the child <div>" + ); + }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`); + })(); + + (() => { + const initialInnerHTML = + `<b>abc${lineBreak}${lineBreak}</b></b><div id="child">[]def<br>ghi</div>`; + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + const b = editingHost.querySelector("b"); + await runDeleteTest( + t, testUtils, initialInnerHTML, + [ + `<b>abc${lineBreak}</b><div id="child">def<br>ghi</div>`, + `<b>abc</b><div id="child">def<br>ghi</div>`, + ], + "should delete only the preceding empty line of the child <div> (<b> should stay)", + [ + `<b>abc${lineBreak}</b><div id="child">Xdef<br>ghi</div>`, + `<b>abc</b><div id="child">Xdef<br>ghi</div>`, + `<b>abc${lineBreak}</b><div id="child"><b>X</b>def<br>ghi</div>`, + `<b>abc</b><div id="child"><b>X</b>def<br>ghi</div>`, + ], + "should insert text into the child <div> with or without <b>", + lineBreakIsBR + ? [ + // <b>abc<br>{<br>}</b><div> + [{ startContainer: b, startOffset: 2, endContainer: b, endOffset: 3 }], + // <b>abc{<br><br>}</b><div> + [{ startContainer: b, startOffset: 1, endContainer: b, endOffset: 3 }], + // <b>abc[<br><br>}</b><div> + [{ startContainer: b.firstChild, startOffset: "abc".length, endContainer: b, endOffset: 3 }], + ] + : [ + // <b>abc\n[\n}</b><div> + [{ startContainer: b.firstChild, startOffset: "abc\n".length, endContainer: b, endOffset: 1 }], + // <b>abc[\n\n}</b><div> + [{ startContainer: b.firstChild, startOffset: "abc".length, endContainer: b, endOffset: 1 }], + // <b>abc\n[\n]</b><div> + [{ startContainer: b.firstChild, startOffset: "abc\n".length, endContainer: b.firstChild, endOffset: "abc\n\n".length }], + // <b>abc[\n\n]</b><div> + [{ startContainer: b.firstChild, startOffset: "abc".length, endContainer: b.firstChild, endOffset: "abc\n\n".length }], + ], + "should return a range before the child <div>" + ); + }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`); + })(); + + (() => { + const initialInnerHTML = + `<b>${lineBreak}</b><div id="child">[]def<br>ghi</div>`; + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + await runDeleteTest( + t, testUtils, initialInnerHTML, + `<div id="child">def<br>ghi</div>`, + "should delete only the preceding empty line (including the <b>) of the child <div>", + [ + `<div id="child">Xdef<br>ghi</div>`, + `<div id="child"><b>X</b>def<br>ghi</div>`, + ], + "should insert text into the child <div> with or without <b>", + [ + // {<b><br></b>}<div> or {<b>\n</b>}<div> + [{ startContainer: editingHost, startOffset: 0, endContainer: editingHost, endOffset: 1 }], + ], + "should return a range before the child <div>" + ); + }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`); + })(); + })(); + + (function test_ForwardDeleteForCollapsedSelection() { + if (testBackward) { + return; + } + (() => { + const initialInnerHTML = + `abc${lineBreak}${selCollapsed}${lineBreak}<div id="child">def<br>ghi</div>`; + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + await runDeleteTest( + t, testUtils, initialInnerHTML, + [ + `abc${lineBreak}<div id="child">def<br>ghi</div>`, + `abc<div id="child">def<br>ghi</div>`, + ], + "should delete only the preceding empty line of the child <div>", + [ + `abc${lineBreak}<div id="child">Xdef<br>ghi</div>`, + `abc<div id="child">Xdef<br>ghi</div>`, + ], + "should insert text into the child <div>", + lineBreakIsBR + ? [ + // abc<br>{<br>}<div> + [{ startContainer: editingHost, startOffset: 2, endContainer: editingHost, endOffset: 3 }], + // abc{<br><br>}<div> + [{ startContainer: editingHost, startOffset: 1, endContainer: editingHost, endOffset: 3 }], + // abc[<br><br>}<div> + [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: editingHost, endOffset: 3 }], + ] + : [ + // abc\n[\n}<div> + [{ startContainer: editingHost.firstChild, startOffset: "abc\n".length, endContainer: editingHost, endOffset: 1 }], + // abc[\n\n}<div> + [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: editingHost, endOffset: 1 }], + // abc\n[\n]<div> + [{ startContainer: editingHost.firstChild, startOffset: "abc\n".length, endContainer: editingHost.firstChild, endOffset: "abc\n\n".length }], + // abc[\n\n]<div> + [{ startContainer: editingHost.firstChild, startOffset: "abc".length, endContainer: editingHost.firstChild, endOffset: "abc\n\n".length }], + ], + "should return a range before the child <div>" + ); + }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`); + })(); + + (() => { + const initialInnerHTML = + `${lineBreak}${selCollapsed}${lineBreak}<div id="child">def<br>ghi</div>`; + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + await runDeleteTest( + t, testUtils, initialInnerHTML, + `${lineBreak}<div id="child">def<br>ghi</div>`, + "should delete only the preceding empty line of the child <div>", + `${lineBreak}<div id="child">Xdef<br>ghi</div>`, + "should insert text into the child <div>", + lineBreakIsBR + ? [ + // <br>{<br>}<div> + [{ startContainer: editingHost, startOffset: 1, endContainer: editingHost, endOffset: 2 }], + ] + : [ + // \n[\n}<div> + [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: editingHost, endOffset: 1 }], + // \n[\n]<div> + [{ startContainer: editingHost.firstChild, startOffset: "\n".length, endContainer: editingHost.firstChild, endOffset: "\n\n".length }], + ], + "should return a range before the child <div>" + ); + }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`); + })(); + + (() => { + const initialInnerHTML = + `${selCollapsed}${lineBreak}<div id="child">def<br>ghi</div>`; + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + await runDeleteTest( + t, testUtils, initialInnerHTML, + `<div id="child">def<br>ghi</div>`, + "should delete only the preceding empty line of the child <div>", + `<div id="child">Xdef<br>ghi</div>`, + "should insert text into the child <div>", + lineBreakIsBR + ? [ + // {<br>}<div> + [{ startContainer: editingHost, startOffset: 0, endContainer: editingHost, endOffset: 1 }], + ] + : [ + // {\n}<div> + [{ startContainer: editingHost, startOffset: 0, endContainer: editingHost, endOffset: 1 }], + // [\n}<div> + [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: editingHost, endOffset: 1 }], + // [\n]<div> + [{ startContainer: editingHost.firstChild, startOffset: 0, endContainer: editingHost.firstChild, endOffset: "\n".length }], + ], + "should return a range before the child <div>" + ); + }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`); + })(); + + (() => { + const initialInnerHTML = + `<b>abc${lineBreak}${selCollapsed}${lineBreak}</b></b><div id="child">def<br>ghi</div>`; + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + const b = editingHost.querySelector("b"); + await runDeleteTest( + t, testUtils, initialInnerHTML, + [ + `<b>abc${lineBreak}</b><div id="child">def<br>ghi</div>`, + `<b>abc</b><div id="child">def<br>ghi</div>`, + ], + "should delete only the preceding empty line of the child <div> (<b> should stay)", + [ + `<b>abc${lineBreak}</b><div id="child">Xdef<br>ghi</div>`, + `<b>abc</b><div id="child">Xdef<br>ghi</div>`, + `<b>abc${lineBreak}</b><div id="child"><b>X</b>def<br>ghi</div>`, + `<b>abc</b><div id="child"><b>X</b>def<br>ghi</div>`, + ], + "should insert text into the child <div> with or without <b>", + lineBreakIsBR + ? [ + // <b>abc<br>{<br>}</b><div> + [{ startContainer: b, startOffset: 2, endContainer: b, endOffset: 3 }], + // <b>abc{<br><br>}</b><div> + [{ startContainer: b, startOffset: 1, endContainer: b, endOffset: 3 }], + // <b>abc[<br><br>}</b><div> + [{ startContainer: b.firstChild, startOffset: "abc".length, endContainer: b, endOffset: 3 }], + ] + : [ + // <b>abc\n[\n}</b><div> + [{ startContainer: b.firstChild, startOffset: "abc\n".length, endContainer: b, endOffset: 1 }], + // <b>abc[\n\n}</b><div> + [{ startContainer: b.firstChild, startOffset: "abc".length, endContainer: b, endOffset: 1 }], + // <b>abc\n[\n]</b><div> + [{ startContainer: b.firstChild, startOffset: "abc\n".length, endContainer: b.firstChild, endOffset: "abc\n\n".length }], + // <b>abc[\n\n]</b><div> + [{ startContainer: b.firstChild, startOffset: "abc".length, endContainer: b.firstChild, endOffset: "abc\n\n".length }], + ], + "should return a range before the child <div>" + ); + }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`); + })(); + + (() => { + const initialInnerHTML = + `<b>${selCollapsed}${lineBreak}</b><div id="child">def<br>ghi</div>`; + promise_test(async t => { + testUtils.setupEditingHost(initialInnerHTML); + await runDeleteTest( + t, testUtils, initialInnerHTML, + `<div id="child">def<br>ghi</div>`, + "should delete only the preceding empty line (including the <b>) of the child <div>", + [ + `<div id="child">Xdef<br>ghi</div>`, + `<div id="child"><b>X</b>def<br>ghi</div>`, + ], + "should insert text into the child <div> with or without <b>", + [ + // {<b><br></b>}<div> or {<b>\n</b>}<div> + [{ startContainer: editingHost, startOffset: 0, endContainer: editingHost, endOffset: 1 }], + ], + "should return a range before the child <div>" + ); + }, `${deleteMethod} at ${initialInnerHTML.replaceAll("\n", "\\\\n")}`); + })(); + })(); +}, {once: true}); +</script> +</head> +<body><div contenteditable></div></body> +</html> diff --git a/tests/wpt/tests/fetch/api/crashtests/huge-fetch.any.js b/tests/wpt/tests/fetch/api/crashtests/huge-fetch.any.js new file mode 100644 index 00000000000..1b09925d855 --- /dev/null +++ b/tests/wpt/tests/fetch/api/crashtests/huge-fetch.any.js @@ -0,0 +1,16 @@ +// META: global=window,worker + +'use strict'; + +promise_test(async t => { + const response = await fetch('../resources/huge-response.py'); + const reader = response.body.getReader(); + // Read one chunk just to show willing. + const { value, done } = await reader.read(); + assert_false(done, 'there should be some data'); + assert_greater_than(value.byteLength, 0, 'the chunk should be non-empty'); + // Wait 2 seconds to give it a chance to crash. + await new Promise(resolve => t.step_timeout(resolve, 2000)); + // If we get here without crashing we passed the test. + reader.cancel(); +}, 'fetching a huge cacheable file but not reading it should not crash'); diff --git a/tests/wpt/tests/fetch/api/request/request-bad-port.any.js b/tests/wpt/tests/fetch/api/request/request-bad-port.any.js index 5c29823eaa4..915063bab56 100644 --- a/tests/wpt/tests/fetch/api/request/request-bad-port.any.js +++ b/tests/wpt/tests/fetch/api/request/request-bad-port.any.js @@ -89,6 +89,6 @@ var BLOCKED_PORTS_LIST = [ BLOCKED_PORTS_LIST.map(function(a){ promise_test(function(t){ - return promise_rejects_js(t, TypeError, fetch("http://example.com:" + a)) + return promise_rejects_js(t, TypeError, fetch(`${location.origin}:${a}`)) }, 'Request on bad port ' + a + ' should throw TypeError.'); }); diff --git a/tests/wpt/tests/fetch/api/resources/huge-response.py b/tests/wpt/tests/fetch/api/resources/huge-response.py new file mode 100644 index 00000000000..16a60078e53 --- /dev/null +++ b/tests/wpt/tests/fetch/api/resources/huge-response.py @@ -0,0 +1,22 @@ +# A Python script that generates a huge response. Implemented as a script to +# avoid needing to add a huge file to the repository. + +TOTAL_SIZE = 8 * 1024 * 1024 * 1024 # 8 GB +CHUNK_SIZE = 1024 * 1024 # 1 MB + +assert TOTAL_SIZE % CHUNK_SIZE == 0 + + +def main(request, response): + response.headers.set(b"Content-type", b"text/plain") + response.headers.set(b"Content-Length", str(TOTAL_SIZE).encode()) + response.headers.set(b"Cache-Control", b"max-age=86400") + response.write_status_headers() + + chunk = bytes(CHUNK_SIZE) + total_sent = 0 + + while total_sent < TOTAL_SIZE: + if not response.writer.write(chunk): + break + total_sent += CHUNK_SIZE diff --git a/tests/wpt/tests/fetch/metadata/generated/appcache-manifest.https.sub.html b/tests/wpt/tests/fetch/metadata/generated/appcache-manifest.https.sub.html deleted file mode 100644 index cf322fd34bc..00000000000 --- a/tests/wpt/tests/fetch/metadata/generated/appcache-manifest.https.sub.html +++ /dev/null @@ -1,341 +0,0 @@ -<!DOCTYPE html> -<!-- -This test was procedurally generated. Please do not modify it directly. -Sources: -- fetch/metadata/tools/fetch-metadata.conf.yml -- fetch/metadata/tools/templates/appcache-manifest.sub.https.html ---> -<html lang="en"> - <meta charset="utf-8"> - <title>HTTP headers on request for Appcache manifest</title> - <script src="/resources/testharness.js"></script> - <script src="/resources/testharnessreport.js"></script> - <script src="/fetch/metadata/resources/helper.sub.js"></script> - <body> - <script> - 'use strict'; - - function induceRequest(url) { - const iframe = document.createElement('iframe'); - iframe.src = - '/fetch/metadata/resources/appcache-iframe.sub.html?manifest=' + encodeURIComponent(url); - - return new Promise((resolve) => { - addEventListener('message', function onMessage(event) { - if (event.source !== iframe.contentWindow) { - return; - } - removeEventListener('message', onMessage); - resolve(event.data); - }); - - document.body.appendChild(iframe); - }) - .then((message) => { - if (message !== 'okay') { - throw message; - } - }) - .then(() => iframe.remove()); - } - - async_test((t) => { - const key = '{{uuid()}}'; - assert_implements_optional( - !!window.applicationCache, 'Application Cache supported.' - ); - - induceRequest(makeRequestURL(key, ['httpsOrigin'])) - .then(() => retrieve(key)) - .then((headers) => { - assert_own_property(headers, 'sec-fetch-site'); - assert_array_equals(headers['sec-fetch-site'], ['same-origin']); - }) - .then(() => t.done(), t.step_func((error) => { throw error; })); - }, 'sec-fetch-site - Same origin'); - - async_test((t) => { - const key = '{{uuid()}}'; - assert_implements_optional( - !!window.applicationCache, 'Application Cache supported.' - ); - - induceRequest(makeRequestURL(key, ['httpsCrossSite'])) - .then(() => retrieve(key)) - .then((headers) => { - assert_own_property(headers, 'sec-fetch-site'); - assert_array_equals(headers['sec-fetch-site'], ['cross-site']); - }) - .then(() => t.done(), t.step_func((error) => { throw error; })); - }, 'sec-fetch-site - Cross-site'); - - async_test((t) => { - const key = '{{uuid()}}'; - assert_implements_optional( - !!window.applicationCache, 'Application Cache supported.' - ); - - induceRequest(makeRequestURL(key, ['httpsSameSite'])) - .then(() => retrieve(key)) - .then((headers) => { - assert_own_property(headers, 'sec-fetch-site'); - assert_array_equals(headers['sec-fetch-site'], ['same-site']); - }) - .then(() => t.done(), t.step_func((error) => { throw error; })); - }, 'sec-fetch-site - Same site'); - - async_test((t) => { - const key = '{{uuid()}}'; - assert_implements_optional( - !!window.applicationCache, 'Application Cache supported.' - ); - - induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpOrigin'])) - .then(() => retrieve(key)) - .then((headers) => { - assert_not_own_property(headers, 'sec-fetch-site'); - }) - .then(() => t.done(), t.step_func((error) => { throw error; })); - }, 'sec-fetch-site - HTTPS downgrade (header not sent)'); - - async_test((t) => { - const key = '{{uuid()}}'; - assert_implements_optional( - !!window.applicationCache, 'Application Cache supported.' - ); - - induceRequest(makeRequestURL(key, ['httpOrigin', 'httpsOrigin'])) - .then(() => retrieve(key)) - .then((headers) => { - assert_own_property(headers, 'sec-fetch-site'); - assert_array_equals(headers['sec-fetch-site'], ['cross-site']); - }) - .then(() => t.done(), t.step_func((error) => { throw error; })); - }, 'sec-fetch-site - HTTPS upgrade'); - - async_test((t) => { - const key = '{{uuid()}}'; - assert_implements_optional( - !!window.applicationCache, 'Application Cache supported.' - ); - - induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpOrigin', 'httpsOrigin'])) - .then(() => retrieve(key)) - .then((headers) => { - assert_own_property(headers, 'sec-fetch-site'); - assert_array_equals(headers['sec-fetch-site'], ['cross-site']); - }) - .then(() => t.done(), t.step_func((error) => { throw error; })); - }, 'sec-fetch-site - HTTPS downgrade-upgrade'); - - async_test((t) => { - const key = '{{uuid()}}'; - assert_implements_optional( - !!window.applicationCache, 'Application Cache supported.' - ); - - induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite', 'httpsOrigin'])) - .then(() => retrieve(key)) - .then((headers) => { - assert_own_property(headers, 'sec-fetch-site'); - assert_array_equals(headers['sec-fetch-site'], ['cross-site']); - }) - .then(() => t.done(), t.step_func((error) => { throw error; })); - }, 'sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect'); - - async_test((t) => { - const key = '{{uuid()}}'; - assert_implements_optional( - !!window.applicationCache, 'Application Cache supported.' - ); - - induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsSameSite', 'httpsOrigin'])) - .then(() => retrieve(key)) - .then((headers) => { - assert_own_property(headers, 'sec-fetch-site'); - assert_array_equals(headers['sec-fetch-site'], ['same-site']); - }) - .then(() => t.done(), t.step_func((error) => { throw error; })); - }, 'sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect'); - - async_test((t) => { - const key = '{{uuid()}}'; - assert_implements_optional( - !!window.applicationCache, 'Application Cache supported.' - ); - - induceRequest(makeRequestURL(key, ['httpsCrossSite', 'httpsOrigin'])) - .then(() => retrieve(key)) - .then((headers) => { - assert_own_property(headers, 'sec-fetch-site'); - assert_array_equals(headers['sec-fetch-site'], ['cross-site']); - }) - .then(() => t.done(), t.step_func((error) => { throw error; })); - }, 'sec-fetch-site - Cross-Site -> Same Origin'); - - async_test((t) => { - const key = '{{uuid()}}'; - assert_implements_optional( - !!window.applicationCache, 'Application Cache supported.' - ); - - induceRequest(makeRequestURL(key, ['httpsCrossSite', 'httpsSameSite'])) - .then(() => retrieve(key)) - .then((headers) => { - assert_own_property(headers, 'sec-fetch-site'); - assert_array_equals(headers['sec-fetch-site'], ['cross-site']); - }) - .then(() => t.done(), t.step_func((error) => { throw error; })); - }, 'sec-fetch-site - Cross-Site -> Same-Site'); - - async_test((t) => { - const key = '{{uuid()}}'; - assert_implements_optional( - !!window.applicationCache, 'Application Cache supported.' - ); - - induceRequest(makeRequestURL(key, ['httpsCrossSite', 'httpsCrossSite'])) - .then(() => retrieve(key)) - .then((headers) => { - assert_own_property(headers, 'sec-fetch-site'); - assert_array_equals(headers['sec-fetch-site'], ['cross-site']); - }) - .then(() => t.done(), t.step_func((error) => { throw error; })); - }, 'sec-fetch-site - Cross-Site -> Cross-Site'); - - async_test((t) => { - const key = '{{uuid()}}'; - assert_implements_optional( - !!window.applicationCache, 'Application Cache supported.' - ); - - induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsOrigin'])) - .then(() => retrieve(key)) - .then((headers) => { - assert_own_property(headers, 'sec-fetch-site'); - assert_array_equals(headers['sec-fetch-site'], ['same-origin']); - }) - .then(() => t.done(), t.step_func((error) => { throw error; })); - }, 'sec-fetch-site - Same-Origin -> Same Origin'); - - async_test((t) => { - const key = '{{uuid()}}'; - assert_implements_optional( - !!window.applicationCache, 'Application Cache supported.' - ); - - induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsSameSite'])) - .then(() => retrieve(key)) - .then((headers) => { - assert_own_property(headers, 'sec-fetch-site'); - assert_array_equals(headers['sec-fetch-site'], ['same-site']); - }) - .then(() => t.done(), t.step_func((error) => { throw error; })); - }, 'sec-fetch-site - Same-Origin -> Same-Site'); - - async_test((t) => { - const key = '{{uuid()}}'; - assert_implements_optional( - !!window.applicationCache, 'Application Cache supported.' - ); - - induceRequest(makeRequestURL(key, ['httpsOrigin', 'httpsCrossSite'])) - .then(() => retrieve(key)) - .then((headers) => { - assert_own_property(headers, 'sec-fetch-site'); - assert_array_equals(headers['sec-fetch-site'], ['cross-site']); - }) - .then(() => t.done(), t.step_func((error) => { throw error; })); - }, 'sec-fetch-site - Same-Origin -> Cross-Site'); - - async_test((t) => { - const key = '{{uuid()}}'; - assert_implements_optional( - !!window.applicationCache, 'Application Cache supported.' - ); - - induceRequest(makeRequestURL(key, ['httpsSameSite', 'httpsOrigin'])) - .then(() => retrieve(key)) - .then((headers) => { - assert_own_property(headers, 'sec-fetch-site'); - assert_array_equals(headers['sec-fetch-site'], ['same-site']); - }) - .then(() => t.done(), t.step_func((error) => { throw error; })); - }, 'sec-fetch-site - Same-Site -> Same Origin'); - - async_test((t) => { - const key = '{{uuid()}}'; - assert_implements_optional( - !!window.applicationCache, 'Application Cache supported.' - ); - - induceRequest(makeRequestURL(key, ['httpsSameSite', 'httpsSameSite'])) - .then(() => retrieve(key)) - .then((headers) => { - assert_own_property(headers, 'sec-fetch-site'); - assert_array_equals(headers['sec-fetch-site'], ['same-site']); - }) - .then(() => t.done(), t.step_func((error) => { throw error; })); - }, 'sec-fetch-site - Same-Site -> Same-Site'); - - async_test((t) => { - const key = '{{uuid()}}'; - assert_implements_optional( - !!window.applicationCache, 'Application Cache supported.' - ); - - induceRequest(makeRequestURL(key, ['httpsSameSite', 'httpsCrossSite'])) - .then(() => retrieve(key)) - .then((headers) => { - assert_own_property(headers, 'sec-fetch-site'); - assert_array_equals(headers['sec-fetch-site'], ['cross-site']); - }) - .then(() => t.done(), t.step_func((error) => { throw error; })); - }, 'sec-fetch-site - Same-Site -> Cross-Site'); - - async_test((t) => { - const key = '{{uuid()}}'; - assert_implements_optional( - !!window.applicationCache, 'Application Cache supported.' - ); - - induceRequest(makeRequestURL(key, [])) - .then(() => retrieve(key)) - .then((headers) => { - assert_own_property(headers, 'sec-fetch-mode'); - assert_array_equals(headers['sec-fetch-mode'], ['no-cors']); - }) - .then(() => t.done(), t.step_func((error) => { throw error; })); - }, 'sec-fetch-mode'); - - async_test((t) => { - const key = '{{uuid()}}'; - assert_implements_optional( - !!window.applicationCache, 'Application Cache supported.' - ); - - induceRequest(makeRequestURL(key, [])) - .then(() => retrieve(key)) - .then((headers) => { - assert_own_property(headers, 'sec-fetch-dest'); - assert_array_equals(headers['sec-fetch-dest'], ['empty']); - }) - .then(() => t.done(), t.step_func((error) => { throw error; })); - }, 'sec-fetch-dest'); - - async_test((t) => { - const key = '{{uuid()}}'; - assert_implements_optional( - !!window.applicationCache, 'Application Cache supported.' - ); - - induceRequest(makeRequestURL(key, [])) - .then(() => retrieve(key)) - .then((headers) => { - assert_not_own_property(headers, 'sec-fetch-user'); - }) - .then(() => t.done(), t.step_func((error) => { throw error; })); - }, 'sec-fetch-user'); - </script> - </body> -</html> diff --git a/tests/wpt/tests/fetch/metadata/generated/worker-dedicated-constructor.sub.html b/tests/wpt/tests/fetch/metadata/generated/worker-dedicated-constructor.sub.html index 69ac7682a5c..65b1837c636 100644 --- a/tests/wpt/tests/fetch/metadata/generated/worker-dedicated-constructor.sub.html +++ b/tests/wpt/tests/fetch/metadata/generated/worker-dedicated-constructor.sub.html @@ -40,36 +40,6 @@ Sources: const key = '{{uuid()}}'; const url = makeRequestURL( key, - ['httpSameSite'], - { mime: 'application/javascript', body: 'postMessage("")' } - ); - - return induceRequest(url) - .then(() => retrieve(key)) - .then((headers) => { - assert_not_own_property(headers, 'sec-fetch-site'); - }); - }, 'sec-fetch-site - Not sent to non-trustworthy same-site destination, no options'); - - promise_test(() => { - const key = '{{uuid()}}'; - const url = makeRequestURL( - key, - ['httpCrossSite'], - { mime: 'application/javascript', body: 'postMessage("")' } - ); - - return induceRequest(url) - .then(() => retrieve(key)) - .then((headers) => { - assert_not_own_property(headers, 'sec-fetch-site'); - }); - }, 'sec-fetch-site - Not sent to non-trustworthy cross-site destination, no options'); - - promise_test(() => { - const key = '{{uuid()}}'; - const url = makeRequestURL( - key, ['httpOrigin'], { mime: 'application/javascript', body: 'postMessage("")' } ); @@ -85,36 +55,6 @@ Sources: const key = '{{uuid()}}'; const url = makeRequestURL( key, - ['httpSameSite'], - { mime: 'application/javascript', body: 'postMessage("")' } - ); - - return induceRequest(url) - .then(() => retrieve(key)) - .then((headers) => { - assert_not_own_property(headers, 'sec-fetch-mode'); - }); - }, 'sec-fetch-mode - Not sent to non-trustworthy same-site destination, no options'); - - promise_test(() => { - const key = '{{uuid()}}'; - const url = makeRequestURL( - key, - ['httpCrossSite'], - { mime: 'application/javascript', body: 'postMessage("")' } - ); - - return induceRequest(url) - .then(() => retrieve(key)) - .then((headers) => { - assert_not_own_property(headers, 'sec-fetch-mode'); - }); - }, 'sec-fetch-mode - Not sent to non-trustworthy cross-site destination, no options'); - - promise_test(() => { - const key = '{{uuid()}}'; - const url = makeRequestURL( - key, ['httpOrigin'], { mime: 'application/javascript', body: 'postMessage("")' } ); @@ -130,36 +70,6 @@ Sources: const key = '{{uuid()}}'; const url = makeRequestURL( key, - ['httpSameSite'], - { mime: 'application/javascript', body: 'postMessage("")' } - ); - - return induceRequest(url) - .then(() => retrieve(key)) - .then((headers) => { - assert_not_own_property(headers, 'sec-fetch-dest'); - }); - }, 'sec-fetch-dest - Not sent to non-trustworthy same-site destination, no options'); - - promise_test(() => { - const key = '{{uuid()}}'; - const url = makeRequestURL( - key, - ['httpCrossSite'], - { mime: 'application/javascript', body: 'postMessage("")' } - ); - - return induceRequest(url) - .then(() => retrieve(key)) - .then((headers) => { - assert_not_own_property(headers, 'sec-fetch-dest'); - }); - }, 'sec-fetch-dest - Not sent to non-trustworthy cross-site destination, no options'); - - promise_test(() => { - const key = '{{uuid()}}'; - const url = makeRequestURL( - key, ['httpOrigin'], { mime: 'application/javascript', body: 'postMessage("")' } ); @@ -170,35 +80,5 @@ Sources: assert_not_own_property(headers, 'sec-fetch-user'); }); }, 'sec-fetch-user - Not sent to non-trustworthy same-origin destination, no options'); - - promise_test(() => { - const key = '{{uuid()}}'; - const url = makeRequestURL( - key, - ['httpSameSite'], - { mime: 'application/javascript', body: 'postMessage("")' } - ); - - return induceRequest(url) - .then(() => retrieve(key)) - .then((headers) => { - assert_not_own_property(headers, 'sec-fetch-user'); - }); - }, 'sec-fetch-user - Not sent to non-trustworthy same-site destination, no options'); - - promise_test(() => { - const key = '{{uuid()}}'; - const url = makeRequestURL( - key, - ['httpCrossSite'], - { mime: 'application/javascript', body: 'postMessage("")' } - ); - - return induceRequest(url) - .then(() => retrieve(key)) - .then((headers) => { - assert_not_own_property(headers, 'sec-fetch-user'); - }); - }, 'sec-fetch-user - Not sent to non-trustworthy cross-site destination, no options'); </script> </html> diff --git a/tests/wpt/tests/fetch/metadata/tools/fetch-metadata.conf.yml b/tests/wpt/tests/fetch/metadata/tools/fetch-metadata.conf.yml index b277bcb7b53..11e61403436 100644 --- a/tests/wpt/tests/fetch/metadata/tools/fetch-metadata.conf.yml +++ b/tests/wpt/tests/fetch/metadata/tools/fetch-metadata.conf.yml @@ -43,8 +43,6 @@ cases: origins: [httpCrossSite] description: Not sent to non-trustworthy cross-site destination template_axes: - # Unused - appcache-manifest.sub.https.html: [] # The `audioWorklet` interface is only available in secure contexts # https://webaudio.github.io/web-audio-api/#BaseAudioContext audioworklet.https.sub.html: [] @@ -91,6 +89,63 @@ cases: svg-image.sub.html: [{}] window-history.sub.html: [{}] worker-dedicated-importscripts.sub.html: [{}] + # `new Worker()` only makes same-origin requests, therefore we split it + # out into the next block. + worker-dedicated-constructor.sub.html: [] + + - all_subtests: + expected: NULL + filename_flags: [] + common_axis: + - headerName: sec-fetch-site + origins: [httpOrigin] + description: Not sent to non-trustworthy same-origin destination + - headerName: sec-fetch-mode + origins: [httpOrigin] + description: Not sent to non-trustworthy same-origin destination + - headerName: sec-fetch-dest + origins: [httpOrigin] + description: Not sent to non-trustworthy same-origin destination + - headerName: sec-fetch-user + origins: [httpOrigin] + description: Not sent to non-trustworthy same-origin destination + template_axes: + # All the templates in this block are unused with the exception of + # `worker-dedicated-constructor` + appcache-manifest.sub.https.html: [] + audioworklet.https.sub.html: [] + fetch-via-serviceworker.https.sub.html: [] + serviceworker.https.sub.html: [] + css-images.sub.html: [] + css-font-face.sub.html: [] + element-a.sub.html: [] + element-area.sub.html: [] + element-audio.sub.html: [] + element-embed.sub.html: [] + element-frame.sub.html: [] + element-iframe.sub.html: [] + element-img.sub.html: [] + element-img-environment-change.sub.html: [] + element-input-image.sub.html: [] + element-link-icon.sub.html: [] + element-link-prefetch.optional.sub.html: [] + element-meta-refresh.optional.sub.html: [] + element-picture.sub.html: [] + element-script.sub.html: [] + element-video.sub.html: [] + element-video-poster.sub.html: [] + fetch.sub.html: [] + form-submission.sub.html: [] + header-link.sub.html: [] + header-refresh.optional.sub.html: [] + window-location.sub.html: [] + script-module-import-dynamic.sub.html: [] + script-module-import-static.sub.html: [] + svg-image.sub.html: [] + window-history.sub.html: [] + worker-dedicated-importscripts.sub.html: [] + # `new Worker()` only makes same-origin requests, so we populate its + # generated tests here. worker-dedicated-constructor.sub.html: [{}] # Sec-Fetch-Site - direct requests @@ -117,7 +172,6 @@ cases: # https://html.spec.whatwg.org/#fetch-a-single-module-script worker-dedicated-constructor.sub.html: [] - appcache-manifest.sub.https.html: [{}] audioworklet.https.sub.html: [{}] css-images.sub.html: - filename_flags: [tentative] @@ -196,7 +250,6 @@ cases: # https://html.spec.whatwg.org/#fetch-a-single-module-script worker-dedicated-constructor.sub.html: [] - appcache-manifest.sub.https.html: [{}] css-images.sub.html: - filename_flags: [tentative] css-font-face.sub.html: @@ -289,7 +342,6 @@ cases: # https://html.spec.whatwg.org/#fetch-a-single-module-script worker-dedicated-constructor.sub.html: [] - appcache-manifest.sub.https.html: [{}] audioworklet.https.sub.html: [{}] css-images.sub.html: - filename_flags: [tentative] @@ -377,7 +429,6 @@ cases: worker-dedicated-constructor.sub.html: [] worker-dedicated-importscripts.sub.html: [] # Avoid duplicate subtest for 'sec-fetch-site - HTTPS downgrade-upgrade' - appcache-manifest.sub.https.html: [] css-images.sub.html: - filename_flags: [tentative] element-a.sub.html: [{}] @@ -409,8 +460,6 @@ cases: filename_flags: [https] origins: [] template_axes: - appcache-manifest.sub.https.html: - - expected: no-cors audioworklet.https.sub.html: # https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-single-module-script - expected: cors @@ -586,8 +635,6 @@ cases: filename_flags: [https] origins: [] template_axes: - appcache-manifest.sub.https.html: - - expected: empty audioworklet.https.sub.html: # https://github.com/WebAudio/web-audio-api/issues/2203 - expected: audioworklet @@ -709,8 +756,6 @@ cases: filename_flags: [https] origins: [] template_axes: - appcache-manifest.sub.https.html: - - expected: NULL audioworklet.https.sub.html: - expected: NULL css-images.sub.html: diff --git a/tests/wpt/tests/fetch/metadata/tools/templates/appcache-manifest.sub.https.html b/tests/wpt/tests/fetch/metadata/tools/templates/appcache-manifest.sub.https.html deleted file mode 100644 index 0dfc084f2e3..00000000000 --- a/tests/wpt/tests/fetch/metadata/tools/templates/appcache-manifest.sub.https.html +++ /dev/null @@ -1,63 +0,0 @@ -<!DOCTYPE html> -<!-- -[%provenance%] ---> -<html lang="en"> - <meta charset="utf-8"> - <title>HTTP headers on request for Appcache manifest</title> - <script src="/resources/testharness.js"></script> - <script src="/resources/testharnessreport.js"></script> - <script src="/fetch/metadata/resources/helper.sub.js"></script> - <body> - <script> - 'use strict'; - - function induceRequest(url) { - const iframe = document.createElement('iframe'); - iframe.src = - '/fetch/metadata/resources/appcache-iframe.sub.html?manifest=' + encodeURIComponent(url); - - return new Promise((resolve) => { - addEventListener('message', function onMessage(event) { - if (event.source !== iframe.contentWindow) { - return; - } - removeEventListener('message', onMessage); - resolve(event.data); - }); - - document.body.appendChild(iframe); - }) - .then((message) => { - if (message !== 'okay') { - throw message; - } - }) - .then(() => iframe.remove()); - } - - {%- for subtest in subtests %} - - async_test((t) => { - const key = '{{uuid()}}'; - assert_implements_optional( - !!window.applicationCache, 'Application Cache supported.' - ); - - induceRequest(makeRequestURL(key, [% subtest.origins %])) - .then(() => retrieve(key)) - .then((headers) => { - {%- if subtest.expected == none %} - assert_not_own_property(headers, '[%subtest.headerName%]'); - {%- else %} - assert_own_property(headers, '[%subtest.headerName%]'); - assert_array_equals(headers['[%subtest.headerName%]'], ['[%subtest.expected%]']); - {%- endif %} - }) - .then(() => t.done(), t.step_func((error) => { throw error; })); - }, '[%subtest.headerName%][%subtest.description | pad("start", " - ")%]'); - - {%- endfor %} - </script> - </body> -</html> diff --git a/tests/wpt/tests/geolocation-API/disabled-by-permissions-policy.https.sub.html b/tests/wpt/tests/geolocation-API/disabled-by-permissions-policy.https.sub.html index f374a58f785..d3d94e1d3d7 100644 --- a/tests/wpt/tests/geolocation-API/disabled-by-permissions-policy.https.sub.html +++ b/tests/wpt/tests/geolocation-API/disabled-by-permissions-policy.https.sub.html @@ -14,9 +14,11 @@ const cross_origin_src = "https://{{hosts[alt][]}}:{{ports[https][0]}}" + same_origin_src; - promise_test(async (t) => { + promise_setup(async () => { await test_driver.set_permission({ name: "geolocation" }, "granted"); + }); + promise_test(async (test) => { const posError = await new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition(reject, resolve); }); @@ -46,22 +48,25 @@ ); }, "Permissions-Policy header geolocation=() disallows the top-level document."); - async_test((t) => { - test_feature_availability( - "geolocation", - t, - same_origin_src, - expect_feature_unavailable_default - ); + promise_test(async (test) => { + await test_feature_availability({ + feature_description: "Geolocation API", + test, + src: same_origin_src, + expect_feature_available: expect_feature_unavailable_default, + is_promise_test: true, + }); }, "Permissions-Policy header geolocation=() disallows same-origin iframes."); - async_test((t) => { - test_feature_availability( - "geolocation", - t, - cross_origin_src, - expect_feature_unavailable_default - ); + promise_test(async (test) => { + await test_feature_availability({ + feature_description: "Geolocation API", + test, + src: cross_origin_src, + expect_feature_available: expect_feature_unavailable_default, + feature_name: "geolocation", + is_promise_test: true, + }); }, "Permissions-Policy header geolocation=() disallows cross-origin iframes."); </script> </body> diff --git a/tests/wpt/tests/geolocation-API/enabled-by-permission-policy-attribute-redirect-on-load.https.sub.html b/tests/wpt/tests/geolocation-API/enabled-by-permission-policy-attribute-redirect-on-load.https.sub.html index c2a2b09fc65..d25afa52bb7 100644 --- a/tests/wpt/tests/geolocation-API/enabled-by-permission-policy-attribute-redirect-on-load.https.sub.html +++ b/tests/wpt/tests/geolocation-API/enabled-by-permission-policy-attribute-redirect-on-load.https.sub.html @@ -1,36 +1,36 @@ <!DOCTYPE html> <body> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="/permissions-policy/resources/permissions-policy.js"></script> -<script> - "use strict"; + <script src="/resources/testharness.js"></script> + <script src="/resources/testharnessreport.js"></script> + <script src="/permissions-policy/resources/permissions-policy.js"></script> + <script> + "use strict"; - const relative_path = "/permissions-policy/resources/permissions-policy-geolocation.html"; - const base_src = "/permissions-policy/resources/redirect-on-load.html#"; - const same_origin_src = base_src + relative_path; - const cross_origin_src = - `${base_src}https://{{hosts[alt][]}}:{{ports[https][0]}}${relative_path}`; + const relative_path = + "/permissions-policy/resources/permissions-policy-geolocation.html"; + const base_src = "/permissions-policy/resources/redirect-on-load.html#"; + const same_origin_src = base_src + relative_path; + const cross_origin_src = `${base_src}https://{{hosts[alt][]}}:{{ports[https][0]}}${relative_path}`; - async_test(t => { - test_feature_availability( - 'geolocation', - t, - same_origin_src, - expect_feature_available_default, - "geolocation" - ); - }, 'Permissions-Policy allow="geolocation" allows same-origin relocation'); + promise_test(async (test) => { + await test_feature_availability({ + feature_description: "Geolocation API", + test, + src: same_origin_src, + expect_feature_available: expect_feature_available_default, + is_promise_test: true, + }); + }, 'Permissions-Policy allow="geolocation" allows same-origin redirection'); - async_test(t => { - test_feature_availability( - 'geolocation', - t, - cross_origin_src, - expect_feature_available_default, - "geolocation" - ); - }, 'Permissions-Policy allow="geolocation" allows cross-origin relocation'); - -</script> + promise_test(async (test) => { + await test_feature_availability({ + feature_description: "Geolocation API", + test, + src: cross_origin_src, + expect_feature_available: expect_feature_available_default, + is_promise_test: true, + feature_name: "geolocation", + }); + }, 'Permissions-Policy allow="geolocation" allows cross-origin redirection'); + </script> </body> diff --git a/tests/wpt/tests/geolocation-API/enabled-by-permissions-policy.https.sub.html b/tests/wpt/tests/geolocation-API/enabled-by-permissions-policy.https.sub.html index d5f72eb1ccb..332e4cea16b 100644 --- a/tests/wpt/tests/geolocation-API/enabled-by-permissions-policy.https.sub.html +++ b/tests/wpt/tests/geolocation-API/enabled-by-permissions-policy.https.sub.html @@ -12,34 +12,40 @@ const cross_origin_src = "https://{{hosts[alt][]}}:{{ports[https][0]}}" + same_origin_src; - promise_test(async (t) => { + promise_setup(async () => { await test_driver.set_permission({ name: "geolocation" }, "granted"); - const result = await new Promise((resolve, reject) => { + }); + + promise_test(async (test) => { + const position = await new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition(resolve, reject); }); assert_true( - result instanceof GeolocationPosition, + position instanceof GeolocationPosition, "Expected a GeolocationPosition" ); }, "Permissions-Policy header geolocation=* allows the top-level document."); - async_test((t) => { - test_feature_availability( - "geolocation", - t, - same_origin_src, - expect_feature_available_default - ); + promise_test(async (test) => { + await test_feature_availability({ + feature_description: "Geolocation API", + test, + src: same_origin_src, + expect_feature_available: expect_feature_available_default, + is_promise_test: true, + }); }, "Permissions-Policy header geolocation=* allows same-origin iframes."); - async_test((t) => { - test_feature_availability( - "geolocation", - t, - cross_origin_src, - expect_feature_available_default - ); + promise_test(async (test) => { + await test_feature_availability({ + feature_description: "Geolocation API", + test, + src: cross_origin_src, + expect_feature_available: expect_feature_available_default, + feature_name: "geolocation", + is_promise_test: true, + }); }, "Permissions-Policy header geolocation=* allows cross-origin iframes."); </script> </body> diff --git a/tests/wpt/tests/geolocation-API/enabled-on-self-origin-by-permissions-policy.https.sub.html b/tests/wpt/tests/geolocation-API/enabled-on-self-origin-by-permissions-policy.https.sub.html index ea4b65966f1..5940888b356 100644 --- a/tests/wpt/tests/geolocation-API/enabled-on-self-origin-by-permissions-policy.https.sub.html +++ b/tests/wpt/tests/geolocation-API/enabled-on-self-origin-by-permissions-policy.https.sub.html @@ -14,29 +14,40 @@ const cross_origin_src = "https://{{hosts[alt][]}}:{{ports[https][0]}}" + same_origin_src; - promise_test(async (t) => { + + promise_setup(async () => { await test_driver.set_permission({ name: "geolocation" }, "granted"); - await new Promise((resolve, reject) => { + }); + + promise_test(async (t) => { + const position = await new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition(resolve, reject); }); + assert_true( + position instanceof GeolocationPosition, + "Expected a GeolocationPosition" + ); }, "Permissions-Policy header geolocation=(self) allows the top-level document."); - async_test((t) => { - test_feature_availability( - "geolocation", - t, - same_origin_src, - expect_feature_available_default - ); + promise_test(async (test) => { + await test_feature_availability({ + feature_description: "Geolocation API", + test, + src: same_origin_src, + expect_feature_available: expect_feature_available_default, + is_promise_test: true, + }); }, "Permissions-Policy header geolocation=(self) allows same-origin iframes."); - async_test((t) => { - test_feature_availability( - "geolocation", - t, - cross_origin_src, - expect_feature_unavailable_default - ); + promise_test(async (test) => { + await test_feature_availability({ + feature_description: "Geolocation API", + test, + src: cross_origin_src, + expect_feature_available: expect_feature_unavailable_default, + feature_name: "geolocation", + is_promise_test: true, + }); }, "Permissions-Policy header geolocation=(self) disallows cross-origin iframes."); </script> </body> diff --git a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-1.html b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-1.html deleted file mode 100644 index 59d66c383c9..00000000000 --- a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-1.html +++ /dev/null @@ -1,29 +0,0 @@ -<!DOCTYPE html> -<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> -<title>Canvas test: 2d.fillStyle.parse.hsl-clamp-1</title> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="/html/canvas/resources/canvas-tests.js"></script> -<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css"> -<body class="show_output"> - -<h1>2d.fillStyle.parse.hsl-clamp-1</h1> -<p class="desc"></p> - -<p class="notes"> -<p class="output">Actual output:</p> -<canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas> -<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsl-clamp-1.png" class="output expected" id="expected" alt=""> -<ul id="d"></ul> -<script> -var t = async_test(""); -_addTest(function(canvas, ctx) { - - ctx.fillStyle = '#f00'; - ctx.fillStyle = 'hsl(120, 200%, 50%)'; - ctx.fillRect(0, 0, 100, 50); - _assertPixel(canvas, 50,25, 0,255,0,255); - -}); -</script> - diff --git a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-3.html b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-3.html deleted file mode 100644 index 56f3a0a8b54..00000000000 --- a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-3.html +++ /dev/null @@ -1,29 +0,0 @@ -<!DOCTYPE html> -<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> -<title>Canvas test: 2d.fillStyle.parse.hsl-clamp-3</title> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="/html/canvas/resources/canvas-tests.js"></script> -<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css"> -<body class="show_output"> - -<h1>2d.fillStyle.parse.hsl-clamp-3</h1> -<p class="desc"></p> - -<p class="notes"> -<p class="output">Actual output:</p> -<canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas> -<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsl-clamp-3.png" class="output expected" id="expected" alt=""> -<ul id="d"></ul> -<script> -var t = async_test(""); -_addTest(function(canvas, ctx) { - - ctx.fillStyle = '#f00'; - ctx.fillStyle = 'hsl(120, 100%, 200%)'; - ctx.fillRect(0, 0, 100, 50); - _assertPixel(canvas, 50,25, 255,255,255,255); - -}); -</script> - diff --git a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-3.png b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-3.png Binary files differdeleted file mode 100644 index bf48767a881..00000000000 --- a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-3.png +++ /dev/null diff --git a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-4.html b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-4.html deleted file mode 100644 index af9d11e678c..00000000000 --- a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-4.html +++ /dev/null @@ -1,29 +0,0 @@ -<!DOCTYPE html> -<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> -<title>Canvas test: 2d.fillStyle.parse.hsl-clamp-4</title> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="/html/canvas/resources/canvas-tests.js"></script> -<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css"> -<body class="show_output"> - -<h1>2d.fillStyle.parse.hsl-clamp-4</h1> -<p class="desc"></p> - -<p class="notes"> -<p class="output">Actual output:</p> -<canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas> -<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsl-clamp-4.png" class="output expected" id="expected" alt=""> -<ul id="d"></ul> -<script> -var t = async_test(""); -_addTest(function(canvas, ctx) { - - ctx.fillStyle = '#f00'; - ctx.fillStyle = 'hsl(120, 100%, -200%)'; - ctx.fillRect(0, 0, 100, 50); - _assertPixel(canvas, 50,25, 0,0,0,255); - -}); -</script> - diff --git a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-4.png b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-4.png Binary files differdeleted file mode 100644 index d638d033868..00000000000 --- a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-4.png +++ /dev/null diff --git a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-2.html b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.html index 1a1939e47af..65440c6228e 100644 --- a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-2.html +++ b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.html @@ -1,19 +1,19 @@ <!DOCTYPE html> <!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> -<title>Canvas test: 2d.fillStyle.parse.hsl-clamp-2</title> +<title>Canvas test: 2d.fillStyle.parse.hsl-clamp-negative-saturation</title> <script src="/resources/testharness.js"></script> <script src="/resources/testharnessreport.js"></script> <script src="/html/canvas/resources/canvas-tests.js"></script> <link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css"> <body class="show_output"> -<h1>2d.fillStyle.parse.hsl-clamp-2</h1> +<h1>2d.fillStyle.parse.hsl-clamp-negative-saturation</h1> <p class="desc"></p> <p class="notes"> <p class="output">Actual output:</p> <canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas> -<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsl-clamp-2.png" class="output expected" id="expected" alt=""> +<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsl-clamp-negative-saturation.png" class="output expected" id="expected" alt=""> <ul id="d"></ul> <script> var t = async_test(""); diff --git a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-2.png b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.png Binary files differindex 88fd8279855..88fd8279855 100644 --- a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-2.png +++ b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.png diff --git a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-1.html b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-1.html deleted file mode 100644 index 2acac26e1a9..00000000000 --- a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-1.html +++ /dev/null @@ -1,29 +0,0 @@ -<!DOCTYPE html> -<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> -<title>Canvas test: 2d.fillStyle.parse.hsla-clamp-1</title> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="/html/canvas/resources/canvas-tests.js"></script> -<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css"> -<body class="show_output"> - -<h1>2d.fillStyle.parse.hsla-clamp-1</h1> -<p class="desc"></p> - -<p class="notes"> -<p class="output">Actual output:</p> -<canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas> -<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsla-clamp-1.png" class="output expected" id="expected" alt=""> -<ul id="d"></ul> -<script> -var t = async_test(""); -_addTest(function(canvas, ctx) { - - ctx.fillStyle = '#f00'; - ctx.fillStyle = 'hsla(120, 200%, 50%, 1)'; - ctx.fillRect(0, 0, 100, 50); - _assertPixel(canvas, 50,25, 0,255,0,255); - -}); -</script> - diff --git a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-1.png b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-1.png Binary files differdeleted file mode 100644 index 2733836c998..00000000000 --- a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-1.png +++ /dev/null diff --git a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-3.html b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-3.html deleted file mode 100644 index 4bc134aec5c..00000000000 --- a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-3.html +++ /dev/null @@ -1,29 +0,0 @@ -<!DOCTYPE html> -<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> -<title>Canvas test: 2d.fillStyle.parse.hsla-clamp-3</title> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="/html/canvas/resources/canvas-tests.js"></script> -<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css"> -<body class="show_output"> - -<h1>2d.fillStyle.parse.hsla-clamp-3</h1> -<p class="desc"></p> - -<p class="notes"> -<p class="output">Actual output:</p> -<canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas> -<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsla-clamp-3.png" class="output expected" id="expected" alt=""> -<ul id="d"></ul> -<script> -var t = async_test(""); -_addTest(function(canvas, ctx) { - - ctx.fillStyle = '#f00'; - ctx.fillStyle = 'hsla(120, 100%, 200%, 1)'; - ctx.fillRect(0, 0, 100, 50); - _assertPixel(canvas, 50,25, 255,255,255,255); - -}); -</script> - diff --git a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-3.png b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-3.png Binary files differdeleted file mode 100644 index bf48767a881..00000000000 --- a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-3.png +++ /dev/null diff --git a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-4.html b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-4.html deleted file mode 100644 index f8b2382755a..00000000000 --- a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-4.html +++ /dev/null @@ -1,29 +0,0 @@ -<!DOCTYPE html> -<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> -<title>Canvas test: 2d.fillStyle.parse.hsla-clamp-4</title> -<script src="/resources/testharness.js"></script> -<script src="/resources/testharnessreport.js"></script> -<script src="/html/canvas/resources/canvas-tests.js"></script> -<link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css"> -<body class="show_output"> - -<h1>2d.fillStyle.parse.hsla-clamp-4</h1> -<p class="desc"></p> - -<p class="notes"> -<p class="output">Actual output:</p> -<canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas> -<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsla-clamp-4.png" class="output expected" id="expected" alt=""> -<ul id="d"></ul> -<script> -var t = async_test(""); -_addTest(function(canvas, ctx) { - - ctx.fillStyle = '#f00'; - ctx.fillStyle = 'hsla(120, 100%, -200%, 1)'; - ctx.fillRect(0, 0, 100, 50); - _assertPixel(canvas, 50,25, 0,0,0,255); - -}); -</script> - diff --git a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-4.png b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-4.png Binary files differdeleted file mode 100644 index d638d033868..00000000000 --- a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-4.png +++ /dev/null diff --git a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-5.png b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-5.png Binary files differdeleted file mode 100644 index 2733836c998..00000000000 --- a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-5.png +++ /dev/null diff --git a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-5.html b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.html index 9c5e2258b9e..e5dc98d4e3a 100644 --- a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-5.html +++ b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.html @@ -1,19 +1,19 @@ <!DOCTYPE html> <!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> -<title>Canvas test: 2d.fillStyle.parse.hsla-clamp-5</title> +<title>Canvas test: 2d.fillStyle.parse.hsla-clamp-alpha-1</title> <script src="/resources/testharness.js"></script> <script src="/resources/testharnessreport.js"></script> <script src="/html/canvas/resources/canvas-tests.js"></script> <link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css"> <body class="show_output"> -<h1>2d.fillStyle.parse.hsla-clamp-5</h1> +<h1>2d.fillStyle.parse.hsla-clamp-alpha-1</h1> <p class="desc"></p> <p class="notes"> <p class="output">Actual output:</p> <canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas> -<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsla-clamp-5.png" class="output expected" id="expected" alt=""> +<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsla-clamp-alpha-1.png" class="output expected" id="expected" alt=""> <ul id="d"></ul> <script> var t = async_test(""); diff --git a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-1.png b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.png Binary files differindex 2733836c998..2733836c998 100644 --- a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-1.png +++ b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.png diff --git a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-6.html b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.html index 153515eedda..26139a562ee 100644 --- a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-6.html +++ b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.html @@ -1,19 +1,19 @@ <!DOCTYPE html> <!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> -<title>Canvas test: 2d.fillStyle.parse.hsla-clamp-6</title> +<title>Canvas test: 2d.fillStyle.parse.hsla-clamp-alpha-2</title> <script src="/resources/testharness.js"></script> <script src="/resources/testharnessreport.js"></script> <script src="/html/canvas/resources/canvas-tests.js"></script> <link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css"> <body class="show_output"> -<h1>2d.fillStyle.parse.hsla-clamp-6</h1> +<h1>2d.fillStyle.parse.hsla-clamp-alpha-2</h1> <p class="desc"></p> <p class="notes"> <p class="output">Actual output:</p> <canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas> -<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsla-clamp-6.png" class="output expected" id="expected" alt=""> +<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsla-clamp-alpha-2.png" class="output expected" id="expected" alt=""> <ul id="d"></ul> <script> var t = async_test(""); diff --git a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-6.png b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.png Binary files differindex eeedd0ff058..eeedd0ff058 100644 --- a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-6.png +++ b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.png diff --git a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-2.html b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.html index 0f32fb5474a..2d9b9d3bdf9 100644 --- a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-2.html +++ b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.html @@ -1,19 +1,19 @@ <!DOCTYPE html> <!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> -<title>Canvas test: 2d.fillStyle.parse.hsla-clamp-2</title> +<title>Canvas test: 2d.fillStyle.parse.hsla-clamp-negative-saturation</title> <script src="/resources/testharness.js"></script> <script src="/resources/testharnessreport.js"></script> <script src="/html/canvas/resources/canvas-tests.js"></script> <link rel="stylesheet" href="/html/canvas/resources/canvas-tests.css"> <body class="show_output"> -<h1>2d.fillStyle.parse.hsla-clamp-2</h1> +<h1>2d.fillStyle.parse.hsla-clamp-negative-saturation</h1> <p class="desc"></p> <p class="notes"> <p class="output">Actual output:</p> <canvas id="c" class="output" width="100" height="50"><p class="fallback">FAIL (fallback content)</p></canvas> -<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsla-clamp-2.png" class="output expected" id="expected" alt=""> +<p class="output expectedtext">Expected output:<p><img src="2d.fillStyle.parse.hsla-clamp-negative-saturation.png" class="output expected" id="expected" alt=""> <ul id="d"></ul> <script> var t = async_test(""); diff --git a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-2.png b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.png Binary files differindex 88fd8279855..88fd8279855 100644 --- a/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-2.png +++ b/tests/wpt/tests/html/canvas/element/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.png diff --git a/tests/wpt/tests/html/canvas/element/text/WEB_FEATURES.yml b/tests/wpt/tests/html/canvas/element/text/WEB_FEATURES.yml new file mode 100644 index 00000000000..1d9e4bab821 --- /dev/null +++ b/tests/wpt/tests/html/canvas/element/text/WEB_FEATURES.yml @@ -0,0 +1,4 @@ +features: +- name: canvas-text-baselines + files: + - 2d.text.measure.baselines.* diff --git a/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.html b/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.html new file mode 100644 index 00000000000..69277677b96 --- /dev/null +++ b/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> +<title>OffscreenCanvas test: 2d.fillStyle.parse.hsl-clamp-negative-saturation</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/html/canvas/resources/canvas-tests.js"></script> + +<h1>2d.fillStyle.parse.hsl-clamp-negative-saturation</h1> +<p class="desc"></p> + +<p class="notes"> +<script> +var t = async_test(""); +var t_pass = t.done.bind(t); +var t_fail = t.step_func(function(reason) { + throw reason; +}); +t.step(function() { + + var canvas = new OffscreenCanvas(100, 50); + var ctx = canvas.getContext('2d'); + + ctx.fillStyle = '#f00'; + ctx.fillStyle = 'hsl(120, -200%, 49.9%)'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 127,127,127,255); + t.done(); + +}); +</script> diff --git a/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.worker.js b/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.worker.js new file mode 100644 index 00000000000..7f1e91ddb7b --- /dev/null +++ b/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsl-clamp-negative-saturation.worker.js @@ -0,0 +1,25 @@ +// DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. +// OffscreenCanvas test in a worker:2d.fillStyle.parse.hsl-clamp-negative-saturation +// Description: +// Note:<p class="notes"> + +importScripts("/resources/testharness.js"); +importScripts("/html/canvas/resources/canvas-tests.js"); + +var t = async_test(""); +var t_pass = t.done.bind(t); +var t_fail = t.step_func(function(reason) { + throw reason; +}); +t.step(function() { + + var canvas = new OffscreenCanvas(100, 50); + var ctx = canvas.getContext('2d'); + + ctx.fillStyle = '#f00'; + ctx.fillStyle = 'hsl(120, -200%, 49.9%)'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 127,127,127,255); + t.done(); +}); +done(); diff --git a/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.html b/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.html new file mode 100644 index 00000000000..5d73d34c9c6 --- /dev/null +++ b/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> +<title>OffscreenCanvas test: 2d.fillStyle.parse.hsla-clamp-alpha-1</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/html/canvas/resources/canvas-tests.js"></script> + +<h1>2d.fillStyle.parse.hsla-clamp-alpha-1</h1> +<p class="desc"></p> + +<p class="notes"> +<script> +var t = async_test(""); +var t_pass = t.done.bind(t); +var t_fail = t.step_func(function(reason) { + throw reason; +}); +t.step(function() { + + var canvas = new OffscreenCanvas(100, 50); + var ctx = canvas.getContext('2d'); + + ctx.fillStyle = '#f00'; + ctx.fillStyle = 'hsla(120, 100%, 50%, 2)'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + t.done(); + +}); +</script> diff --git a/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.worker.js b/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.worker.js new file mode 100644 index 00000000000..7acb76d80b8 --- /dev/null +++ b/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-1.worker.js @@ -0,0 +1,25 @@ +// DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. +// OffscreenCanvas test in a worker:2d.fillStyle.parse.hsla-clamp-alpha-1 +// Description: +// Note:<p class="notes"> + +importScripts("/resources/testharness.js"); +importScripts("/html/canvas/resources/canvas-tests.js"); + +var t = async_test(""); +var t_pass = t.done.bind(t); +var t_fail = t.step_func(function(reason) { + throw reason; +}); +t.step(function() { + + var canvas = new OffscreenCanvas(100, 50); + var ctx = canvas.getContext('2d'); + + ctx.fillStyle = '#f00'; + ctx.fillStyle = 'hsla(120, 100%, 50%, 2)'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,255,0,255); + t.done(); +}); +done(); diff --git a/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.html b/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.html new file mode 100644 index 00000000000..eaf7a6af89c --- /dev/null +++ b/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> +<title>OffscreenCanvas test: 2d.fillStyle.parse.hsla-clamp-alpha-2</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/html/canvas/resources/canvas-tests.js"></script> + +<h1>2d.fillStyle.parse.hsla-clamp-alpha-2</h1> +<p class="desc"></p> + +<p class="notes"> +<script> +var t = async_test(""); +var t_pass = t.done.bind(t); +var t_fail = t.step_func(function(reason) { + throw reason; +}); +t.step(function() { + + var canvas = new OffscreenCanvas(100, 50); + var ctx = canvas.getContext('2d'); + + ctx.fillStyle = '#f00'; + ctx.fillStyle = 'hsla(120, 100%, 0%, -2)'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,0,0,0); + t.done(); + +}); +</script> diff --git a/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.worker.js b/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.worker.js new file mode 100644 index 00000000000..540b3ea15f0 --- /dev/null +++ b/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-alpha-2.worker.js @@ -0,0 +1,25 @@ +// DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. +// OffscreenCanvas test in a worker:2d.fillStyle.parse.hsla-clamp-alpha-2 +// Description: +// Note:<p class="notes"> + +importScripts("/resources/testharness.js"); +importScripts("/html/canvas/resources/canvas-tests.js"); + +var t = async_test(""); +var t_pass = t.done.bind(t); +var t_fail = t.step_func(function(reason) { + throw reason; +}); +t.step(function() { + + var canvas = new OffscreenCanvas(100, 50); + var ctx = canvas.getContext('2d'); + + ctx.fillStyle = '#f00'; + ctx.fillStyle = 'hsla(120, 100%, 0%, -2)'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 0,0,0,0); + t.done(); +}); +done(); diff --git a/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.html b/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.html new file mode 100644 index 00000000000..04749fb4a79 --- /dev/null +++ b/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<!-- DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. --> +<title>OffscreenCanvas test: 2d.fillStyle.parse.hsla-clamp-negative-saturation</title> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/html/canvas/resources/canvas-tests.js"></script> + +<h1>2d.fillStyle.parse.hsla-clamp-negative-saturation</h1> +<p class="desc"></p> + +<p class="notes"> +<script> +var t = async_test(""); +var t_pass = t.done.bind(t); +var t_fail = t.step_func(function(reason) { + throw reason; +}); +t.step(function() { + + var canvas = new OffscreenCanvas(100, 50); + var ctx = canvas.getContext('2d'); + + ctx.fillStyle = '#f00'; + ctx.fillStyle = 'hsla(120, -200%, 49.9%, 1)'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 127,127,127,255); + t.done(); + +}); +</script> diff --git a/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.worker.js b/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.worker.js new file mode 100644 index 00000000000..f5fe6d4296d --- /dev/null +++ b/tests/wpt/tests/html/canvas/offscreen/fill-and-stroke-styles/2d.fillStyle.parse.hsla-clamp-negative-saturation.worker.js @@ -0,0 +1,25 @@ +// DO NOT EDIT! This test has been generated by /html/canvas/tools/gentest.py. +// OffscreenCanvas test in a worker:2d.fillStyle.parse.hsla-clamp-negative-saturation +// Description: +// Note:<p class="notes"> + +importScripts("/resources/testharness.js"); +importScripts("/html/canvas/resources/canvas-tests.js"); + +var t = async_test(""); +var t_pass = t.done.bind(t); +var t_fail = t.step_func(function(reason) { + throw reason; +}); +t.step(function() { + + var canvas = new OffscreenCanvas(100, 50); + var ctx = canvas.getContext('2d'); + + ctx.fillStyle = '#f00'; + ctx.fillStyle = 'hsla(120, -200%, 49.9%, 1)'; + ctx.fillRect(0, 0, 100, 50); + _assertPixel(canvas, 50,25, 127,127,127,255); + t.done(); +}); +done(); diff --git a/tests/wpt/tests/html/canvas/offscreen/text/WEB_FEATURES.yml b/tests/wpt/tests/html/canvas/offscreen/text/WEB_FEATURES.yml new file mode 100644 index 00000000000..1d9e4bab821 --- /dev/null +++ b/tests/wpt/tests/html/canvas/offscreen/text/WEB_FEATURES.yml @@ -0,0 +1,4 @@ +features: +- name: canvas-text-baselines + files: + - 2d.text.measure.baselines.* diff --git a/tests/wpt/tests/html/canvas/tools/yaml/element/meta.yaml b/tests/wpt/tests/html/canvas/tools/yaml/element/meta.yaml index 5fd8b68498a..12852e200ad 100644 --- a/tests/wpt/tests/html/canvas/tools/yaml/element/meta.yaml +++ b/tests/wpt/tests/html/canvas/tools/yaml/element/meta.yaml @@ -390,18 +390,12 @@ ('hsl-4', 'hsl(-360240, 100%, 50%)', 0,255,0,255, ""), ('hsl-5', 'hsl(120.0, 100.0%, 50.0%)', 0,255,0,255, ""), ('hsl-6', 'hsl(+120, +100%, +50%)', 0,255,0,255, ""), - ('hsl-clamp-1', 'hsl(120, 200%, 50%)', 0,255,0,255, ""), - ('hsl-clamp-2', 'hsl(120, -200%, 49.9%)', 127,127,127,255, ""), - ('hsl-clamp-3', 'hsl(120, 100%, 200%)', 255,255,255,255, ""), - ('hsl-clamp-4', 'hsl(120, 100%, -200%)', 0,0,0,255, ""), + ('hsl-clamp-negative-saturation', 'hsl(120, -200%, 49.9%)', 127,127,127,255, ""), ('hsla-1', 'hsla(120, 100%, 50%, 0.499)', 0,255,0,127, ""), ('hsla-2', 'hsla( 120.0 , 100.0% , 50.0% , 1 )', 0,255,0,255, ""), - ('hsla-clamp-1', 'hsla(120, 200%, 50%, 1)', 0,255,0,255, ""), - ('hsla-clamp-2', 'hsla(120, -200%, 49.9%, 1)', 127,127,127,255, ""), - ('hsla-clamp-3', 'hsla(120, 100%, 200%, 1)', 255,255,255,255, ""), - ('hsla-clamp-4', 'hsla(120, 100%, -200%, 1)', 0,0,0,255, ""), - ('hsla-clamp-5', 'hsla(120, 100%, 50%, 2)', 0,255,0,255, ""), - ('hsla-clamp-6', 'hsla(120, 100%, 0%, -2)', 0,0,0,0, ""), + ('hsla-clamp-negative-saturation', 'hsla(120, -200%, 49.9%, 1)', 127,127,127,255, ""), + ('hsla-clamp-alpha-1', 'hsla(120, 100%, 50%, 2)', 0,255,0,255, ""), + ('hsla-clamp-alpha-2', 'hsla(120, 100%, 0%, -2)', 0,0,0,0, ""), ('svg-1', 'gray', 128,128,128,255, ""), ('svg-2', 'grey', 128,128,128,255, ""), # css-color-4 rgb() color function diff --git a/tests/wpt/tests/html/canvas/tools/yaml/offscreen/meta.yaml b/tests/wpt/tests/html/canvas/tools/yaml/offscreen/meta.yaml index 7b44fd9f267..b07898224d2 100644 --- a/tests/wpt/tests/html/canvas/tools/yaml/offscreen/meta.yaml +++ b/tests/wpt/tests/html/canvas/tools/yaml/offscreen/meta.yaml @@ -346,18 +346,12 @@ ('hsl-4', 'hsl(-360240, 100%, 50%)', 0,255,0,255, ""), ('hsl-5', 'hsl(120.0, 100.0%, 50.0%)', 0,255,0,255, ""), ('hsl-6', 'hsl(+120, +100%, +50%)', 0,255,0,255, ""), - ('hsl-clamp-1', 'hsl(120, 200%, 50%)', 0,255,0,255, ""), - ('hsl-clamp-2', 'hsl(120, -200%, 49.9%)', 127,127,127,255, ""), - ('hsl-clamp-3', 'hsl(120, 100%, 200%)', 255,255,255,255, ""), - ('hsl-clamp-4', 'hsl(120, 100%, -200%)', 0,0,0,255, ""), + ('hsl-clamp-negative-saturation', 'hsl(120, -200%, 49.9%)', 127,127,127,255, ""), ('hsla-1', 'hsla(120, 100%, 50%, 0.499)', 0,255,0,127, ""), ('hsla-2', 'hsla( 120.0 , 100.0% , 50.0% , 1 )', 0,255,0,255, ""), - ('hsla-clamp-1', 'hsla(120, 200%, 50%, 1)', 0,255,0,255, ""), - ('hsla-clamp-2', 'hsla(120, -200%, 49.9%, 1)', 127,127,127,255, ""), - ('hsla-clamp-3', 'hsla(120, 100%, 200%, 1)', 255,255,255,255, ""), - ('hsla-clamp-4', 'hsla(120, 100%, -200%, 1)', 0,0,0,255, ""), - ('hsla-clamp-5', 'hsla(120, 100%, 50%, 2)', 0,255,0,255, ""), - ('hsla-clamp-6', 'hsla(120, 100%, 0%, -2)', 0,0,0,0, ""), + ('hsla-clamp-negative-saturation', 'hsla(120, -200%, 49.9%, 1)', 127,127,127,255, ""), + ('hsla-clamp-alpha-1', 'hsla(120, 100%, 50%, 2)', 0,255,0,255, ""), + ('hsla-clamp-alpha-2', 'hsla(120, 100%, 0%, -2)', 0,0,0,0, ""), ('svg-1', 'gray', 128,128,128,255, ""), ('svg-2', 'grey', 128,128,128,255, ""), # css-color-4 rgb() color function diff --git a/tests/wpt/tests/html/dom/aria-element-reflection.html b/tests/wpt/tests/html/dom/aria-element-reflection.html index bdf2450708b..e04610171b5 100644 --- a/tests/wpt/tests/html/dom/aria-element-reflection.html +++ b/tests/wpt/tests/html/dom/aria-element-reflection.html @@ -611,7 +611,7 @@ </script> <div id="sameScopeContainer"> - <div id="labeledby" aria-labeledby="headingLabel1 headingLabel2">Misspelling</div> + <div id="labelledby" aria-labelledby="headingLabel1 headingLabel2">Misspelling</div> <div id="headingLabel1">Wonderful</div> <div id="headingLabel2">Fantastic</div> @@ -626,7 +626,7 @@ const headingLabel2 = document.getElementById("headingLabel2") shadowRoot.appendChild(headingElement); - assert_array_equals(labeledby.ariaLabelledByElements, [headingLabel1, headingLabel2], "aria-labeled by is supported by IDL getter."); + assert_array_equals(labelledby.ariaLabelledByElements, [headingLabel1, headingLabel2], "aria-labelledby is supported by IDL getter."); // Explicitly set elements are in a lighter shadow DOM, so that's ok. headingElement.ariaLabelledByElements = [headingLabel1, headingLabel2]; diff --git a/tests/wpt/tests/html/dom/usvstring-reflection.https.html b/tests/wpt/tests/html/dom/usvstring-reflection.https.html index 775cb49281e..d8d830dc597 100644 --- a/tests/wpt/tests/html/dom/usvstring-reflection.https.html +++ b/tests/wpt/tests/html/dom/usvstring-reflection.https.html @@ -124,7 +124,7 @@ promise_test(t => { const sendString = 'hello\uD999'; const receiveString = 'hello\uFFFD'; - return createDataChannelPair(t) + return createDataChannelPair(t, {}) .then(([channel1, channel2]) => { channel1.send(sendString); return awaitMessage(channel2) diff --git a/tests/wpt/tests/html/editing/dnd/events/drag-event-div-manual.html b/tests/wpt/tests/html/editing/dnd/events/drag-event-div-manual.html index 79c0c4332d0..505b0049bee 100644 --- a/tests/wpt/tests/html/editing/dnd/events/drag-event-div-manual.html +++ b/tests/wpt/tests/html/editing/dnd/events/drag-event-div-manual.html @@ -6,7 +6,6 @@ <link rel="author" title="Microsoft" href="http://www.microsoft.com/"/> <link rel="help" href="http://dev.w3.org/html5/spec/dnd.html#drag-and-drop-processing-model"/> <meta name="assert" content="Fire drag event when dragging a div element"/> - <script src="../resources/dragdrop_support.js" type="text/javascript"></script> <script type="text/javascript"> var EVENT, TARGET; @@ -14,11 +13,11 @@ { if ((TARGET == evt.target) && (EVENT == evt.type)) { - LogTestResult("PASS"); + document.getElementById("test_result").firstChild.data = "PASS"; } else { - LogTestResult("FAIL"); + document.getElementById("test_result").firstChild.data = "FAIL"; } } @@ -27,7 +26,7 @@ window.onload = function() { TARGET = document.getElementById("target"); - AddEventListenersForElement(EVENT, DragEvent, false, TARGET); + TARGET.addEventListener(EVENT, DragEvent, false); } </script> </head> diff --git a/tests/wpt/tests/html/editing/dnd/events/drag-event-manual.html b/tests/wpt/tests/html/editing/dnd/events/drag-event-manual.html index d278b864bb9..d3f517ea1d5 100644 --- a/tests/wpt/tests/html/editing/dnd/events/drag-event-manual.html +++ b/tests/wpt/tests/html/editing/dnd/events/drag-event-manual.html @@ -6,7 +6,6 @@ <link rel="author" title="Microsoft" href="http://www.microsoft.com/"/> <link rel="help" href="http://dev.w3.org/html5/spec/dnd.html#drag-and-drop-processing-model"/> <meta name="assert" content="Fire drag event during the drag and drop processing"/> - <script src="../resources/dragdrop_support.js" type="text/javascript"></script> <script type="text/javascript"> var EVENT, TARGET; @@ -14,11 +13,11 @@ { if ((TARGET == evt.target) && (EVENT == evt.type)) { - LogTestResult("PASS"); + document.getElementById("test_result").firstChild.data = "PASS"; } else { - LogTestResult("FAIL"); + document.getElementById("test_result").firstChild.data = "FAIL"; } } @@ -27,7 +26,7 @@ window.onload = function() { TARGET = document.getElementById("target"); - AddEventListenersForElement(EVENT, DragEvent, false, TARGET); + TARGET.addEventListener(EVENT, DragEvent, false); } </script> </head> diff --git a/tests/wpt/tests/html/editing/dnd/events/dragend-event-manual.html b/tests/wpt/tests/html/editing/dnd/events/dragend-event-manual.html index 8bfb1fb7b6e..b4bb621e888 100644 --- a/tests/wpt/tests/html/editing/dnd/events/dragend-event-manual.html +++ b/tests/wpt/tests/html/editing/dnd/events/dragend-event-manual.html @@ -6,7 +6,6 @@ <link rel="author" title="Microsoft" href="http://www.microsoft.com/"/> <link rel="help" href="http://dev.w3.org/html5/spec/dnd.html#drag-and-drop-processing-model"/> <meta name="assert" content="Fire dragend event during the drag and drop processing"/> - <script src="../resources/dragdrop_support.js" type="text/javascript"></script> <script type="text/javascript"> var EVENT, TARGET; @@ -14,11 +13,11 @@ { if ((TARGET == evt.target) && (EVENT == evt.type)) { - LogTestResult("PASS"); + document.getElementById("test_result").firstChild.data = "PASS"; } else { - LogTestResult("FAIL"); + document.getElementById("test_result").firstChild.data = "FAIL"; } } @@ -27,7 +26,7 @@ window.onload = function() { TARGET = document.getElementById("target"); - AddEventListenersForElement(EVENT, DragendEvent, false, TARGET); + TARGET.addEventListener(EVENT, DragendEvent, false); } </script> </head> diff --git a/tests/wpt/tests/html/editing/dnd/events/dragenter-event-manual.html b/tests/wpt/tests/html/editing/dnd/events/dragenter-event-manual.html index e81b32949cc..23b404b0b17 100644 --- a/tests/wpt/tests/html/editing/dnd/events/dragenter-event-manual.html +++ b/tests/wpt/tests/html/editing/dnd/events/dragenter-event-manual.html @@ -6,7 +6,6 @@ <link rel="author" title="Microsoft" href="http://www.microsoft.com/"/> <link rel="help" href="http://dev.w3.org/html5/spec/dnd.html#drag-and-drop-processing-model"/> <meta name="assert" content="Fire dragenter event during the drag and drop processing"/> - <script src="../resources/dragdrop_support.js" type="text/javascript"></script> <script type="text/javascript"> var EVENT, TARGET; @@ -14,11 +13,11 @@ { if ((TARGET == evt.target) && (EVENT == evt.type)) { - LogTestResult("PASS"); + document.getElementById("test_result").firstChild.data = "PASS"; } else { - LogTestResult("FAIL"); + document.getElementById("test_result").firstChild.data = "FAIL"; } } @@ -27,7 +26,7 @@ window.onload = function() { TARGET = document.getElementById("target"); - AddEventListenersForElement(EVENT, DragenterEvent, false, TARGET); + TARGET.addEventListener(EVENT, DragenterEvent, false); } </script> </head> diff --git a/tests/wpt/tests/html/editing/dnd/events/dragleave-event-manual.html b/tests/wpt/tests/html/editing/dnd/events/dragleave-event-manual.html index f6a405915fc..a400fa3417c 100644 --- a/tests/wpt/tests/html/editing/dnd/events/dragleave-event-manual.html +++ b/tests/wpt/tests/html/editing/dnd/events/dragleave-event-manual.html @@ -6,7 +6,6 @@ <link rel="author" title="Microsoft" href="http://www.microsoft.com/"/> <link rel="help" href="http://dev.w3.org/html5/spec/dnd.html#drag-and-drop-processing-model"/> <meta name="assert" content="Fire dragleave event during the drag and drop processing"/> - <script src="../resources/dragdrop_support.js" type="text/javascript"></script> <script type="text/javascript"> var EVENT, TARGET; @@ -14,11 +13,11 @@ { if ((TARGET == evt.target) && (EVENT == evt.type)) { - LogTestResult("PASS"); + document.getElementById("test_result").firstChild.data = "PASS"; } else { - LogTestResult("FAIL"); + document.getElementById("test_result").firstChild.data = "FAIL"; } } @@ -27,7 +26,7 @@ window.onload = function() { TARGET = document.getElementById("target"); - AddEventListenersForElement(EVENT, DragleaveEvent, false, TARGET); + TARGET.addEventListener(EVENT, DragleaveEvent, false); } </script> </head> diff --git a/tests/wpt/tests/html/editing/dnd/events/dragover-event-manual.html b/tests/wpt/tests/html/editing/dnd/events/dragover-event-manual.html index f8d99241d59..f37a33cff65 100644 --- a/tests/wpt/tests/html/editing/dnd/events/dragover-event-manual.html +++ b/tests/wpt/tests/html/editing/dnd/events/dragover-event-manual.html @@ -6,7 +6,6 @@ <link rel="author" title="Microsoft" href="http://www.microsoft.com/"/> <link rel="help" href="http://dev.w3.org/html5/spec/dnd.html#drag-and-drop-processing-model"/> <meta name="assert" content="Fire dragover event during the drag and drop processing"/> - <script src="../resources/dragdrop_support.js" type="text/javascript"></script> <script type="text/javascript"> var EVENT, TARGET; @@ -14,11 +13,11 @@ { if ((TARGET == evt.target) && (EVENT == evt.type)) { - LogTestResult("PASS"); + document.getElementById("test_result").firstChild.data = "PASS"; } else { - LogTestResult("FAIL"); + document.getElementById("test_result").firstChild.data = "FAIL"; } } @@ -27,7 +26,7 @@ window.onload = function() { TARGET = document.getElementById("target"); - AddEventListenersForElement(EVENT, DragoverEvent, false, TARGET); + TARGET.addEventListener(EVENT, DragoverEvent, false); } </script> </head> diff --git a/tests/wpt/tests/html/editing/dnd/events/dragstart-event-manual.html b/tests/wpt/tests/html/editing/dnd/events/dragstart-event-manual.html index 20786648da3..9128401ffaa 100644 --- a/tests/wpt/tests/html/editing/dnd/events/dragstart-event-manual.html +++ b/tests/wpt/tests/html/editing/dnd/events/dragstart-event-manual.html @@ -6,7 +6,6 @@ <link rel="author" title="Microsoft" href="http://www.microsoft.com/"/> <link rel="help" href="http://dev.w3.org/html5/spec/dnd.html#drag-and-drop-processing-model"/> <meta name="assert" content="Fire dragstart event during the drag and drop processing"/> - <script src="../resources/dragdrop_support.js" type="text/javascript"></script> <script type="text/javascript"> var EVENT, TARGET; @@ -14,11 +13,11 @@ { if ((TARGET == evt.target) && (EVENT == evt.type)) { - LogTestResult("PASS"); + document.getElementById("test_result").firstChild.data = "PASS"; } else { - LogTestResult("FAIL"); + document.getElementById("test_result").firstChild.data = "FAIL"; } } @@ -27,7 +26,7 @@ window.onload = function() { TARGET = document.getElementById("target"); - AddEventListenersForElement(EVENT, DragstartEvent, false, TARGET); + TARGET.addEventListener(EVENT, DragstartEvent, false); } </script> </head> diff --git a/tests/wpt/tests/html/editing/dnd/events/drop-event-manual.html b/tests/wpt/tests/html/editing/dnd/events/drop-event-manual.html index 2897bd57135..8393e386966 100644 --- a/tests/wpt/tests/html/editing/dnd/events/drop-event-manual.html +++ b/tests/wpt/tests/html/editing/dnd/events/drop-event-manual.html @@ -6,7 +6,6 @@ <link rel="author" title="Microsoft" href="http://www.microsoft.com/"/> <link rel="help" href="http://dev.w3.org/html5/spec/dnd.html#drag-and-drop-processing-model"/> <meta name="assert" content="Fire drop event during the drag and drop processing"/> - <script src="../resources/dragdrop_support.js" type="text/javascript"></script> <script type="text/javascript"> var EVENT, TARGET; @@ -14,11 +13,11 @@ { if ((TARGET == evt.target) && (EVENT == evt.type)) { - LogTestResult("PASS"); + document.getElementById("test_result").firstChild.data = "PASS"; } else { - LogTestResult("FAIL"); + document.getElementById("test_result").firstChild.data = "FAIL"; } } @@ -27,7 +26,7 @@ window.onload = function() { TARGET = document.getElementById("target"); - AddEventListenersForElement(EVENT, DropEvent, false, TARGET); + TARGET.addEventListener(EVENT, DropEvent, false); } </script> </head> diff --git a/tests/wpt/tests/html/editing/dnd/resources/dragdrop_support.js b/tests/wpt/tests/html/editing/dnd/resources/dragdrop_support.js deleted file mode 100644 index f5a1d6417f3..00000000000 --- a/tests/wpt/tests/html/editing/dnd/resources/dragdrop_support.js +++ /dev/null @@ -1,9 +0,0 @@ -function AddEventListenersForElement(evt, callback, capture, element) -{ - element.addEventListener(evt, callback, capture); -} - -function LogTestResult(result) -{ - document.getElementById("test_result").firstChild.data = result; -} diff --git a/tests/wpt/tests/html/editing/dnd/the-datatransfer-interface/effectAllowed-manual.html b/tests/wpt/tests/html/editing/dnd/the-datatransfer-interface/effectAllowed-manual.html index 08540b906a5..61443da2c02 100644 --- a/tests/wpt/tests/html/editing/dnd/the-datatransfer-interface/effectAllowed-manual.html +++ b/tests/wpt/tests/html/editing/dnd/the-datatransfer-interface/effectAllowed-manual.html @@ -6,7 +6,6 @@ <link rel="author" title="Microsoft" href="http://www.microsoft.com/"/> <link rel="help" href="http://dev.w3.org/html5/spec/dnd.html#datatransfer"/> <meta name="assert" content="Set a value to effectAllowed attribute"/> - <script src="../resources/dragdrop_support.js" type="text/javascript"></script> <script type="text/javascript"> var TARGETEVENT1, TARGETEVENT2, TARGET1, TARGET2; @@ -23,11 +22,11 @@ { if("move" == evt.dataTransfer.effectAllowed) { - LogTestResult("PASS"); + document.getElementById("test_result").firstChild.data = "PASS"; } else { - LogTestResult("FAIL"); + document.getElementById("test_result").firstChild.data = "FAIL"; } } } @@ -39,8 +38,8 @@ { TARGET1 = document.getElementById("target1"); TARGET2 = document.getElementById("target2"); - AddEventListenersForElement(TARGETEVENT1, DragstartEvent, false, TARGET1); - AddEventListenersForElement(TARGETEVENT2, DragenterEvent, false, TARGET2); + TARGET1.addEventListener(TARGETEVENT1, DragstartEvent, false); + TARGET2.addEventListener(TARGETEVENT2, DragenterEvent, false); } </script> </head> diff --git a/tests/wpt/tests/html/editing/dnd/the-datatransfer-interface/files-manual.html b/tests/wpt/tests/html/editing/dnd/the-datatransfer-interface/files-manual.html index 7de0b4bbce1..ffafb66db4f 100644 --- a/tests/wpt/tests/html/editing/dnd/the-datatransfer-interface/files-manual.html +++ b/tests/wpt/tests/html/editing/dnd/the-datatransfer-interface/files-manual.html @@ -6,7 +6,6 @@ <link rel="author" title="Microsoft" href="http://www.microsoft.com/"/> <link rel="help" href="http://dev.w3.org/html5/spec/dnd.html#datatransfer"/> <meta name="assert" content="files attribute returns a FileList"/> - <script src="../resources/dragdrop_support.js" type="text/javascript"></script> <script type="text/javascript"> var EVENT, TARGET; @@ -17,16 +16,16 @@ var files = evt.dataTransfer.files; if(('[object FileList]' == files)) { - LogTestResult("PASS"); + document.getElementById("test_result").firstChild.data = "PASS"; } else { - LogTestResult("FAIL"); + document.getElementById("test_result").firstChild.data = "FAIL"; } } else { - LogTestResult("FAIL"); + document.getElementById("test_result").firstChild.data = "FAIL"; } } @@ -45,9 +44,9 @@ window.onload = function() { TARGET = document.getElementById("target"); - AddEventListenersForElement(EVENT, DropEvent, false, TARGET); - AddEventListenersForElement("dragenter", DragenterEvent, false, TARGET); - AddEventListenersForElement("dragover", DragoverEvent, false, TARGET); + TARGET.addEventListener(EVENT, DropEvent, false); + TARGET.addEventListener("dragenter", DragenterEvent, false); + TARGET.addEventListener("dragover", DragoverEvent, false); } </script> </head> diff --git a/tests/wpt/tests/html/editing/dnd/the-datatransfer-interface/setData-manual.html b/tests/wpt/tests/html/editing/dnd/the-datatransfer-interface/setData-manual.html index f0f7cae6009..1438e932ad0 100644 --- a/tests/wpt/tests/html/editing/dnd/the-datatransfer-interface/setData-manual.html +++ b/tests/wpt/tests/html/editing/dnd/the-datatransfer-interface/setData-manual.html @@ -6,7 +6,6 @@ <link rel="author" title="Microsoft" href="http://www.microsoft.com/"/> <link rel="help" href="http://dev.w3.org/html5/spec/dnd.html#datatransfer"/> <meta name="assert" content="Add an item to the drag data store item list whose data is the string given by setData method's second argument"/> - <script src="../resources/dragdrop_support.js" type="text/javascript"></script> <script type="text/javascript"> var TARGETEVENT1, TARGETEVENT2, TARGET1, TARGET2; @@ -23,11 +22,11 @@ { if("SetText" == evt.dataTransfer.getData("text")) { - LogTestResult("PASS"); + document.getElementById("test_result").firstChild.data = "PASS"; } else { - LogTestResult("FAIL"); + document.getElementById("test_result").firstChild.data = "FAIL"; } } } @@ -39,8 +38,8 @@ { TARGET1 = document.getElementById("target1"); TARGET2 = document.getElementById("target2"); - AddEventListenersForElement(TARGETEVENT1, DragstartEvent, false, TARGET1); - AddEventListenersForElement(TARGETEVENT2, DropEvent, false, TARGET2); + TARGET1.addEventListener(TARGETEVENT1, DragstartEvent, false); + TARGET2.addEventListener(TARGETEVENT2, DropEvent, false); } </script> </head> diff --git a/tests/wpt/tests/html/editing/dnd/the-datatransfer-interface/types-manual.html b/tests/wpt/tests/html/editing/dnd/the-datatransfer-interface/types-manual.html index 1730c4bc738..3aa1404e296 100644 --- a/tests/wpt/tests/html/editing/dnd/the-datatransfer-interface/types-manual.html +++ b/tests/wpt/tests/html/editing/dnd/the-datatransfer-interface/types-manual.html @@ -6,7 +6,6 @@ <link rel="author" title="Microsoft" href="http://www.microsoft.com/"/> <link rel="help" href="http://dev.w3.org/html5/spec/dnd.html#datatransfer"/> <meta name="assert" content="types attribute returns a DOMStringList"/> - <script src="../resources/dragdrop_support.js" type="text/javascript"></script> <script type="text/javascript"> var EVENT, TARGET; @@ -17,16 +16,16 @@ var types = evt.dataTransfer.types; if(('[object DOMStringList]' == types)) { - LogTestResult("PASS"); + document.getElementById("test_result").firstChild.data = "PASS"; } else { - LogTestResult("FAIL"); + document.getElementById("test_result").firstChild.data = "FAIL"; } } else { - LogTestResult("FAIL"); + document.getElementById("test_result").firstChild.data = "FAIL"; } } @@ -35,7 +34,7 @@ window.onload = function() { TARGET = document.getElementById("target"); - AddEventListenersForElement(EVENT, DropEvent, false, TARGET); + TARGET.addEventListener(EVENT, DropEvent, false); } </script> </head> diff --git a/tests/wpt/tests/html/semantics/forms/constraints/form-validation-validity-textarea-defaultValue.html b/tests/wpt/tests/html/semantics/forms/constraints/form-validation-validity-textarea-defaultValue.html new file mode 100644 index 00000000000..55276116ade --- /dev/null +++ b/tests/wpt/tests/html/semantics/forms/constraints/form-validation-validity-textarea-defaultValue.html @@ -0,0 +1,98 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>textarea validation behavior</title> +<link rel="author" href="mailto:masonf@chromium.org"> +<link rel="help" href="https://html.spec.whatwg.org/multipage/#suffering-from-being-too-short"> +<link rel="help" href="https://html.spec.whatwg.org/multipage/#the-constraint-validation-api"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> + +<textarea id=t1 minlength=5 required></textarea> +<textarea id=t2 minlength=5 required>a</textarea> +<textarea id=t3 required>a</textarea> +<textarea id=t4>a</textarea> +<script> +test(() => { + const emptyMinlength = document.getElementById('t1'); + const nonEmptyMinlength = document.getElementById('t2'); + const nonEmptyRequired = document.getElementById('t3'); + const nonEmptyNonRequired = document.getElementById('t4'); + assert_false(emptyMinlength.validity.valid,'Empty textareas with constraints will validate'); + assert_true(nonEmptyMinlength.validity.valid,'Non-empty textareas with constraints will *not* validate'); + assert_true(nonEmptyRequired.validity.valid,'Textareas without constraints will validate'); + assert_true(nonEmptyNonRequired.validity.valid,'Textareas without constraints will validate'); + [t1,t2,t3,t4].forEach(t => t.remove()); +},'Default validity based on emptiness'); +</script> + +<textarea id=t5 minlength=5 required></textarea> +<script> +promise_test(async () => { + const textarea = document.getElementById('t5'); + document.querySelector('#t1'); + assert_false(textarea.validity.valid,'By default, this textarea will validate (and fail) because it started empty'); + textarea.defaultValue = 'abc'; + assert_true(textarea.validity.valid,'Programmatically setting defaultValue is not a user edit - automatically valid'); + textarea.replaceChildren('abcd'); + assert_true(textarea.validity.valid,'Programmatically replacing children is not a user edit - automatically valid'); + textarea.defaultValue = 'abcde'; + assert_true(textarea.validity.valid,'Still valid'); + textarea.remove(); +},'Setting textarea.defaultValue should not trigger validation'); +</script> + +<textarea id=t6 minlength=5 required></textarea> +<script> +promise_test(async () => { + const textarea = document.getElementById('t6'); + assert_false(textarea.validity.valid,'By default, this textarea will validate (and fail) because it started empty'); + await test_driver.send_keys(textarea, "abc"); + assert_false(textarea.validity.valid,'Keystrokes should trigger validation, which will fail (length 3)'); + await test_driver.send_keys(textarea, "de"); + assert_equals(textarea.value,"abcde"); + assert_true(textarea.validity.valid,'Now valid'); + textarea.remove(); +},'User keystrokes should trigger validation'); +</script> + +<textarea id=t7 minlength=5 required></textarea> +<script> +promise_test(async () => { + const textarea = document.getElementById('t7'); + textarea.addEventListener('input', (e) => { + e.target.defaultValue = e.target.value; + }); + assert_false(textarea.validity.valid,'By default, this textarea will validate (and fail) because it started empty'); + await test_driver.send_keys(textarea, "abc"); + assert_equals(textarea.value,"abc"); + assert_false(textarea.validity.valid,'Still invalid with 3 characters'); + await test_driver.send_keys(textarea, "de"); + assert_equals(textarea.value,"abcde"); + assert_true(textarea.validity.valid,'With 5 characters, now valid'); + textarea.remove(); +},'Setting textarea.defaultValue from the input event handler should trigger validation'); +</script> + +<textarea id=t8 minlength=5 required></textarea> +<script> +promise_test(async () => { + const textarea = document.getElementById('t8'); + textarea.addEventListener('input', (e) => { + e.target.replaceChildren(e.target.value); + }); + assert_false(textarea.validity.valid,'By default, this textarea will validate (and fail) because it started empty'); + await test_driver.send_keys(textarea, "abc"); + assert_equals(textarea.value,"abc"); + assert_false(textarea.validity.valid,'Still invalid with 3 characters'); + await test_driver.send_keys(textarea, "de"); + assert_equals(textarea.value,"abcde"); + assert_true(textarea.validity.valid,'With 5 characters, now valid'); + textarea.remove(); +},'Calling textarea.replaceChildren() from the input event handler should trigger validation'); +</script> + +<style> + :invalid { background-color: rgb(248, 203, 203); } +</style> diff --git a/tests/wpt/tests/html/semantics/forms/the-input-element/input-stepdown-02.html b/tests/wpt/tests/html/semantics/forms/the-input-element/input-stepdown-02.html new file mode 100644 index 00000000000..db71d110092 --- /dev/null +++ b/tests/wpt/tests/html/semantics/forms/the-input-element/input-stepdown-02.html @@ -0,0 +1,42 @@ +<!DOCTYPE HTML> +<title>Input Step Down</title> + +<link rel="help" href="https://html.spec.whatwg.org/multipage/input.html#dom-input-stepup"> + +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> + +<input type='number' id='input'> + +<script> + const input = document.getElementById("input"); + + function testStepDown(initialValue, minValue, expectedValue) { + input.value = initialValue; + input.min = minValue; + + input.stepDown(); + + assert_equals(input.value, expectedValue); + } + + const tests = [ + { initialValue: '', minValue: '', expectedValue: '-1', description: 'stepDown() on input with no initial or min values' }, + { initialValue: '', minValue: '7', expectedValue: '7', description: 'stepDown() on input with no initial value and positive min value' }, + { initialValue: '', minValue: '-7', expectedValue: '-1', description: 'stepDown() on input with no initial value and negative min value' }, + { initialValue: '7', minValue: '7', expectedValue: '7', description: 'stepDown() on input with initial value equal to min value' }, + { initialValue: '3', minValue: '7', expectedValue: '3', description: 'stepDown() on input with initial value less than min value' }, + { initialValue: '10', minValue: '7', expectedValue: '9', description: 'stepDown() on input with initial value greater than min value' }, + ]; + + for(const t of tests) { + test(()=>{ + testStepDown( + t.initialValue, + t.minValue, + t.expectedValue + ); + }, + t.description); + } +</script> diff --git a/tests/wpt/tests/html/semantics/permission-element/bounded-sizes-reftest-ref.html b/tests/wpt/tests/html/semantics/permission-element/bounded-sizes-reftest-ref.html index 1e0104cc5ce..b186dd64458 100644 --- a/tests/wpt/tests/html/semantics/permission-element/bounded-sizes-reftest-ref.html +++ b/tests/wpt/tests/html/semantics/permission-element/bounded-sizes-reftest-ref.html @@ -9,6 +9,8 @@ <li>max-width should be at most 3x min-width</li> <li>min-height should be sufficient to fit the element text (1em)</li> <li>max-height should be at most 3x min-height</li> + <li>padding-left/top only work with width/height: auto and are at most 5em/1em</li> + <li>padding-right/bottom are copied over from padding-left/top in this case</li> </ul> </div> @@ -23,10 +25,21 @@ height: 30px; /* width set via JS */ } + #id3 { + font-size: 10px; + height: 30px; + color:black; + background-color: black; + + /* Used to compute width which will then have the padding + artificially added in JS */ + width: fit-content; + } </style> -<permission id="id1" type="geolocation"> -<permission id="id2" type="camera"> + <div><permission id="id1" type="geolocation"></div> + <div><permission id="id2" type="camera"></div> + <div><permission id="id3" type="microphone"></div> <script> let el = document.getElementById("id1"); @@ -34,6 +47,10 @@ el = document.getElementById("id2"); el.style.width = getComputedStyle(el).maxWidth; + + el = document.getElementById("id3"); + let w = getComputedStyle(el).width; + el.style.width = `calc(${w} + 100px)`; // 100px is 2 * 5em; </script> </body> </html>
\ No newline at end of file diff --git a/tests/wpt/tests/html/semantics/permission-element/bounded-sizes-reftest.tentative.html b/tests/wpt/tests/html/semantics/permission-element/bounded-sizes-reftest.tentative.html index 3d24d30679e..45ffb633c3d 100644 --- a/tests/wpt/tests/html/semantics/permission-element/bounded-sizes-reftest.tentative.html +++ b/tests/wpt/tests/html/semantics/permission-element/bounded-sizes-reftest.tentative.html @@ -10,6 +10,8 @@ <li>max-width should be at most 3x min-width</li> <li>min-height should be sufficient to fit the element text (1em)</li> <li>max-height should be at most 3x min-height</li> + <li>padding-left/top only work with width/height: auto and are at most 5em/1em</li> + <li>padding-right/bottom are copied over from padding-left/top in this case</li> </ul> </div> @@ -40,8 +42,27 @@ width: 1000px; height: 1000px; } + #id3 { + font-size: 10px; + width: auto; + height: auto; + + /* There is a slight misalignment of the text (by 1px) when using + padding vs using width/height. Since this test's purpose is to + check that the bounds are identical, the color and background-color + are set to the same value to cover the slight text misalignment */ + color:black; + background-color: black; + + /* Only padding-top and padding-left are taken into account */ + padding-top: 1000px; + padding-left: 1000px; + padding-bottom: 1px; + padding-right: 1px; + } </style> -<permission id="id1" type="geolocation"> -<permission id="id2" type="camera"> +<div><permission id="id1" type="geolocation"></div> +<div><permission id="id2" type="camera"></div> +<div><permission id="id3" type="microphone"></div> </body>
\ No newline at end of file diff --git a/tests/wpt/tests/html/semantics/permission-element/bounded-sizes.tentative.html b/tests/wpt/tests/html/semantics/permission-element/bounded-sizes.tentative.html index 405e2364f54..2010cd0a544 100644 --- a/tests/wpt/tests/html/semantics/permission-element/bounded-sizes.tentative.html +++ b/tests/wpt/tests/html/semantics/permission-element/bounded-sizes.tentative.html @@ -13,17 +13,33 @@ <style> #id1 { font-size: 10px; + width: auto; + height: auto; + min-height: 1px; max-height: 100px; + padding-top: 12px; + padding-left: 60px; + padding-bottom: 1000px; + padding-right: 1000px; + /* These values are extreme enough that they should be out of bounds for any implementation */ min-width: 10px; max-width: 1000px; } #id2 { font-size: 10px; + width: auto; + height: auto; + min-height: 11px; max-height: 29px; + + padding-top: 5px; + padding-left: 45px; + padding-bottom: 6px; + padding-right: 46px; } </style> @@ -40,12 +56,20 @@ assert_true(max_height === "calc(30px)" || max_height === "30px", "max-height"); assert_not_equals(getComputedStyle(el_outside_bounds).minWidth, "10px", "min-width"); assert_not_equals(getComputedStyle(el_outside_bounds).maxWidth, "1000px", "max-width"); + assert_equals(getComputedStyle(el_outside_bounds).paddingLeft, "50px", "padding-left"); + assert_equals(getComputedStyle(el_outside_bounds).paddingRight, "50px", "padding-right"); + assert_equals(getComputedStyle(el_outside_bounds).paddingTop, "10px", "padding-top"); + assert_equals(getComputedStyle(el_outside_bounds).paddingBottom, "10px", "padding-bottom"); }, "Properties with out-of-bounds values should be corrected"); test(function(){ let el_inside_bounds = document.getElementById("id2"); assert_equals(getComputedStyle(el_inside_bounds).minHeight, "calc(11px)", "min-height"); assert_equals(getComputedStyle(el_inside_bounds).maxHeight, "calc(29px)", "max-height"); + assert_equals(getComputedStyle(el_inside_bounds).paddingLeft, "45px", "padding-left"); + assert_equals(getComputedStyle(el_inside_bounds).paddingRight, "45px", "padding-right"); + assert_equals(getComputedStyle(el_inside_bounds).paddingTop, "5px", "padding-top"); + assert_equals(getComputedStyle(el_inside_bounds).paddingBottom, "5px", "padding-bottom"); }, "Properties with values in bounds should not be modified"); </script> </body>
\ No newline at end of file diff --git a/tests/wpt/tests/html/semantics/selectors/pseudo-classes/WEB_FEATURES.yml b/tests/wpt/tests/html/semantics/selectors/pseudo-classes/WEB_FEATURES.yml index 604e049f202..055a5fb4a30 100644 --- a/tests/wpt/tests/html/semantics/selectors/pseudo-classes/WEB_FEATURES.yml +++ b/tests/wpt/tests/html/semantics/selectors/pseudo-classes/WEB_FEATURES.yml @@ -5,3 +5,7 @@ features: - name: default files: - default.html +- name: read-write-pseudos + files: + - readwrite-readonly-type-change.html + - readwrite-readonly.html diff --git a/tests/wpt/tests/inert/WEB_FEATURES.yml b/tests/wpt/tests/inert/WEB_FEATURES.yml new file mode 100644 index 00000000000..074e33726c6 --- /dev/null +++ b/tests/wpt/tests/inert/WEB_FEATURES.yml @@ -0,0 +1,3 @@ +features: +- name: inert + files: "**" diff --git a/tests/wpt/tests/input-events/input-events-get-target-ranges-deleting-in-list-items.tentative.html b/tests/wpt/tests/input-events/input-events-get-target-ranges-deleting-in-list-items.tentative.html index 7c225cd051d..3572d227f00 100644 --- a/tests/wpt/tests/input-events/input-events-get-target-ranges-deleting-in-list-items.tentative.html +++ b/tests/wpt/tests/input-events/input-events-get-target-ranges-deleting-in-list-items.tentative.html @@ -1020,8 +1020,8 @@ for (let childList of ["ul", "ol"]) { { startContainer: gEditor.querySelector(`${list} > ${childList} > li`).firstChild, startOffset: 0, - endContainer: gEditor.querySelector(`${list} > li`).firstChild, - endOffset: gEditor.querySelector(`${list} > li`).firstChild.length, + endContainer: gEditor.querySelector(`${childList} + li`).firstChild, + endOffset: gEditor.querySelector(`${childList} + li`).firstChild.length, }, ]; }, diff --git a/tests/wpt/tests/interfaces/DOM-Parsing.idl b/tests/wpt/tests/interfaces/DOM-Parsing.idl index d0d84ab6972..676753bf0fc 100644 --- a/tests/wpt/tests/interfaces/DOM-Parsing.idl +++ b/tests/wpt/tests/interfaces/DOM-Parsing.idl @@ -9,18 +9,6 @@ interface XMLSerializer { DOMString serializeToString(Node root); }; -interface mixin InnerHTML { - [CEReactions] attribute [LegacyNullToEmptyString] DOMString innerHTML; -}; - -Element includes InnerHTML; -ShadowRoot includes InnerHTML; - -partial interface Element { - [CEReactions] attribute [LegacyNullToEmptyString] DOMString outerHTML; - [CEReactions] undefined insertAdjacentHTML(DOMString position, DOMString text); -}; - partial interface Range { - [CEReactions, NewObject] DocumentFragment createContextualFragment(DOMString fragment); + [CEReactions, NewObject] DocumentFragment createContextualFragment(HTMLString fragment); }; diff --git a/tests/wpt/tests/interfaces/compute-pressure.idl b/tests/wpt/tests/interfaces/compute-pressure.idl index c4dcb90af43..a90febffc3b 100644 --- a/tests/wpt/tests/interfaces/compute-pressure.idl +++ b/tests/wpt/tests/interfaces/compute-pressure.idl @@ -14,9 +14,9 @@ callback PressureUpdateCallback = undefined ( [Exposed=(DedicatedWorker,SharedWorker,Window), SecureContext] interface PressureObserver { - constructor(PressureUpdateCallback callback, optional PressureObserverOptions options = {}); + constructor(PressureUpdateCallback callback); - Promise<undefined> observe(PressureSource source); + Promise<undefined> observe(PressureSource source, optional PressureObserverOptions options = {}); undefined unobserve(PressureSource source); undefined disconnect(); sequence<PressureRecord> takeRecords(); diff --git a/tests/wpt/tests/interfaces/css-anchor-position.idl b/tests/wpt/tests/interfaces/css-anchor-position.idl index b79e3fce893..5eeaa030b85 100644 --- a/tests/wpt/tests/interfaces/css-anchor-position.idl +++ b/tests/wpt/tests/interfaces/css-anchor-position.idl @@ -6,5 +6,79 @@ [Exposed=Window] interface CSSPositionTryRule : CSSRule { readonly attribute CSSOMString name; - [SameObject, PutForwards=cssText] readonly attribute CSSStyleDeclaration style; + [SameObject, PutForwards=cssText] readonly attribute CSSPositionTryDescriptors style; +}; + +[Exposed=Window] +interface CSSPositionTryDescriptors : CSSStyleDeclaration { + attribute CSSOMString margin; + attribute CSSOMString marginTop; + attribute CSSOMString marginRight; + attribute CSSOMString marginBottom; + attribute CSSOMString marginLeft; + attribute CSSOMString marginBlock; + attribute CSSOMString marginBlockStart; + attribute CSSOMString marginBlockEnd; + attribute CSSOMString marginInline; + attribute CSSOMString marginInlineStart; + attribute CSSOMString marginInlineEnd; + attribute CSSOMString margin-top; + attribute CSSOMString margin-right; + attribute CSSOMString margin-bottom; + attribute CSSOMString margin-left; + attribute CSSOMString margin-block; + attribute CSSOMString margin-block-start; + attribute CSSOMString margin-block-end; + attribute CSSOMString margin-inline; + attribute CSSOMString margin-inline-start; + attribute CSSOMString margin-inline-end; + attribute CSSOMString inset; + attribute CSSOMString insetBlock; + attribute CSSOMString insetBlockStart; + attribute CSSOMString insetBlockEnd; + attribute CSSOMString insetInline; + attribute CSSOMString insetInlineStart; + attribute CSSOMString insetInlineEnd; + attribute CSSOMString top; + attribute CSSOMString left; + attribute CSSOMString right; + attribute CSSOMString bottom; + attribute CSSOMString inset-block; + attribute CSSOMString inset-block-start; + attribute CSSOMString inset-block-end; + attribute CSSOMString inset-inline; + attribute CSSOMString inset-inline-start; + attribute CSSOMString inset-inline-end; + attribute CSSOMString width; + attribute CSSOMString minWidth; + attribute CSSOMString maxWidth; + attribute CSSOMString height; + attribute CSSOMString minHeight; + attribute CSSOMString maxHeight; + attribute CSSOMString blockSize; + attribute CSSOMString minBlockSize; + attribute CSSOMString maxBlockSize; + attribute CSSOMString inlineSize; + attribute CSSOMString minInlineSize; + attribute CSSOMString maxInlineSize; + attribute CSSOMString min-width; + attribute CSSOMString max-width; + attribute CSSOMString min-height; + attribute CSSOMString max-height; + attribute CSSOMString block-size; + attribute CSSOMString min-block-size; + attribute CSSOMString max-block-size; + attribute CSSOMString inline-size; + attribute CSSOMString min-inline-size; + attribute CSSOMString max-inline-size; + attribute CSSOMString placeSelf; + attribute CSSOMString alignSelf; + attribute CSSOMString justifySelf; + attribute CSSOMString place-self; + attribute CSSOMString align-self; + attribute CSSOMString justify-self; + attribute CSSOMString positionAnchor; + attribute CSSOMString position-anchor; + attribute CSSOMString insetArea; + attribute CSSOMString inset-area; }; diff --git a/tests/wpt/tests/interfaces/css-nesting.idl b/tests/wpt/tests/interfaces/css-nesting.idl new file mode 100644 index 00000000000..58d3247f904 --- /dev/null +++ b/tests/wpt/tests/interfaces/css-nesting.idl @@ -0,0 +1,9 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: CSS Nesting Module (https://drafts.csswg.org/css-nesting-1/) + +[Exposed=Window] +interface CSSNestRule : CSSGroupingRule { + [SameObject, PutForwards=cssText] readonly attribute CSSStyleProperties style; +}; diff --git a/tests/wpt/tests/interfaces/css-view-transitions-2.idl b/tests/wpt/tests/interfaces/css-view-transitions-2.idl index 41337f4e1e4..559870751a2 100644 --- a/tests/wpt/tests/interfaces/css-view-transitions-2.idl +++ b/tests/wpt/tests/interfaces/css-view-transitions-2.idl @@ -10,14 +10,9 @@ partial interface CSSRule { enum ViewTransitionNavigation { "auto", "none" }; [Exposed=Window] -interface CSSViewTransitionTypeSet { - readonly setlike<CSSOMString>; -}; - -[Exposed=Window] interface CSSViewTransitionRule : CSSRule { readonly attribute ViewTransitionNavigation navigation; - readonly attribute CSSViewTransitionTypeSet types; + [SameObject] readonly attribute FrozenArray<CSSOMString> types; }; [Exposed=Window] diff --git a/tests/wpt/tests/interfaces/device-attributes.idl b/tests/wpt/tests/interfaces/device-attributes.idl new file mode 100644 index 00000000000..cf62523ad82 --- /dev/null +++ b/tests/wpt/tests/interfaces/device-attributes.idl @@ -0,0 +1,13 @@ +// GENERATED CONTENT - DO NOT EDIT +// Content was automatically extracted by Reffy into webref +// (https://github.com/w3c/webref) +// Source: Device Attributes API (https://wicg.github.io/WebApiDevice/device_attributes/) + +partial interface NavigatorManagedData { + // Device Attributes API. + Promise<DOMString> getAnnotatedAssetId(); + Promise<DOMString> getAnnotatedLocation(); + Promise<DOMString> getDirectoryId(); + Promise<DOMString> getHostname(); + Promise<DOMString> getSerialNumber(); +}; diff --git a/tests/wpt/tests/interfaces/digital-identities.idl b/tests/wpt/tests/interfaces/digital-identities.idl index 9027ce61af8..2d1b7208502 100644 --- a/tests/wpt/tests/interfaces/digital-identities.idl +++ b/tests/wpt/tests/interfaces/digital-identities.idl @@ -17,11 +17,11 @@ dictionary DigitalCredentialRequestOptions { dictionary IdentityRequestProvider { required DOMString protocol; - required DOMString request; + required object request; }; [Exposed=Window, SecureContext] interface DigitalCredential : Credential { readonly attribute DOMString protocol; - readonly attribute DOMString data; + [SameObject] readonly attribute Uint8Array data; }; diff --git a/tests/wpt/tests/interfaces/document-picture-in-picture.idl b/tests/wpt/tests/interfaces/document-picture-in-picture.idl index 888855b38f2..ed34b3c2160 100644 --- a/tests/wpt/tests/interfaces/document-picture-in-picture.idl +++ b/tests/wpt/tests/interfaces/document-picture-in-picture.idl @@ -20,7 +20,7 @@ interface DocumentPictureInPicture : EventTarget { dictionary DocumentPictureInPictureOptions { [EnforceRange] unsigned long long width = 0; [EnforceRange] unsigned long long height = 0; - boolean allowReturnToOpener = true; + boolean disallowReturnToOpener = false; }; [Exposed=Window, SecureContext] diff --git a/tests/wpt/tests/interfaces/dom.idl b/tests/wpt/tests/interfaces/dom.idl index cf2d4e4adc5..72d61f5cfd8 100644 --- a/tests/wpt/tests/interfaces/dom.idl +++ b/tests/wpt/tests/interfaces/dom.idl @@ -120,9 +120,9 @@ interface mixin ParentNode { readonly attribute Element? lastElementChild; readonly attribute unsigned long childElementCount; - [CEReactions, Unscopable] undefined prepend((Node or DOMString)... nodes); - [CEReactions, Unscopable] undefined append((Node or DOMString)... nodes); - [CEReactions, Unscopable] undefined replaceChildren((Node or DOMString)... nodes); + [CEReactions, Unscopable] undefined prepend((Node or TrustedScript or DOMString)... nodes); + [CEReactions, Unscopable] undefined append((Node or TrustedScript or DOMString)... nodes); + [CEReactions, Unscopable] undefined replaceChildren((Node or TrustedScript or DOMString)... nodes); Element? querySelector(DOMString selectors); [NewObject] NodeList querySelectorAll(DOMString selectors); @@ -139,9 +139,9 @@ Element includes NonDocumentTypeChildNode; CharacterData includes NonDocumentTypeChildNode; interface mixin ChildNode { - [CEReactions, Unscopable] undefined before((Node or DOMString)... nodes); - [CEReactions, Unscopable] undefined after((Node or DOMString)... nodes); - [CEReactions, Unscopable] undefined replaceWith((Node or DOMString)... nodes); + [CEReactions, Unscopable] undefined before((Node or TrustedScript or DOMString)... nodes); + [CEReactions, Unscopable] undefined after((Node or TrustedScript or DOMString)... nodes); + [CEReactions, Unscopable] undefined replaceWith((Node or TrustedScript or DOMString)... nodes); [CEReactions, Unscopable] undefined remove(); }; DocumentType includes ChildNode; @@ -340,6 +340,7 @@ interface ShadowRoot : DocumentFragment { readonly attribute boolean delegatesFocus; readonly attribute SlotAssignmentMode slotAssignment; readonly attribute boolean clonable; + readonly attribute boolean serializable; readonly attribute Element host; attribute EventHandler onslotchange; }; @@ -398,6 +399,7 @@ dictionary ShadowRootInit { boolean delegatesFocus = false; SlotAssignmentMode slotAssignment = "named"; boolean clonable = false; + boolean serializable = false; }; [Exposed=Window, diff --git a/tests/wpt/tests/interfaces/gamepad.idl b/tests/wpt/tests/interfaces/gamepad.idl index 024e5ea58c1..d922d7b80b0 100644 --- a/tests/wpt/tests/interfaces/gamepad.idl +++ b/tests/wpt/tests/interfaces/gamepad.idl @@ -44,7 +44,8 @@ enum GamepadHapticsResult { }; enum GamepadHapticEffectType { - "dual-rumble" + "dual-rumble", + "trigger-rumble" }; dictionary GamepadEffectParameters { @@ -52,6 +53,8 @@ dictionary GamepadEffectParameters { unsigned long long startDelay = 0; double strongMagnitude = 0.0; double weakMagnitude = 0.0; + double leftTrigger = 0.0; + double rightTrigger = 0.0; }; [Exposed=Window] diff --git a/tests/wpt/tests/interfaces/html.idl b/tests/wpt/tests/interfaces/html.idl index 2f97e4dd603..e9598a1bbb6 100644 --- a/tests/wpt/tests/interfaces/html.idl +++ b/tests/wpt/tests/interfaces/html.idl @@ -1245,6 +1245,7 @@ interface HTMLTemplateElement : HTMLElement { [CEReactions] attribute DOMString shadowRootMode; [CEReactions] attribute boolean shadowRootDelegatesFocus; [CEReactions] attribute boolean shadowRootClonable; + [CEReactions] attribute boolean shadowRootSerializable; }; [Exposed=Window] @@ -1579,7 +1580,6 @@ interface OffscreenCanvas : EventTarget { [Exposed=(Window,Worker)] interface OffscreenCanvasRenderingContext2D { - undefined commit(); readonly attribute OffscreenCanvas canvas; }; @@ -2292,6 +2292,27 @@ interface mixin WindowOrWorkerGlobalScope { Window includes WindowOrWorkerGlobalScope; WorkerGlobalScope includes WindowOrWorkerGlobalScope; +partial interface Element { + [CEReactions] undefined setHTMLUnsafe(HTMLString html); + DOMString getHTML(optional GetHTMLOptions options = {}); + + [CEReactions] attribute [LegacyNullToEmptyString] HTMLString innerHTML; + [CEReactions] attribute [LegacyNullToEmptyString] HTMLString outerHTML; + [CEReactions] undefined insertAdjacentHTML(DOMString position, HTMLString string); +}; + +partial interface ShadowRoot { + [CEReactions] undefined setHTMLUnsafe(HTMLString html); + DOMString getHTML(optional GetHTMLOptions options = {}); + + [CEReactions] attribute [LegacyNullToEmptyString] HTMLString innerHTML; +}; + +dictionary GetHTMLOptions { + boolean serializableShadowRoots = false; + sequence<ShadowRoot> shadowRoots = []; +}; + [Exposed=Window] interface DOMParser { constructor(); @@ -2307,14 +2328,6 @@ enum DOMParserSupportedType { "image/svg+xml" }; -partial interface Element { - [CEReactions] undefined setHTMLUnsafe(HTMLString html); -}; - -partial interface ShadowRoot { - [CEReactions] undefined setHTMLUnsafe(HTMLString html); -}; - [Exposed=Window] interface Navigator { // objects implementing this interface also implement the interfaces given below diff --git a/tests/wpt/tests/interfaces/mediasession.idl b/tests/wpt/tests/interfaces/mediasession.idl index 8e9a21aff06..e6c8e464627 100644 --- a/tests/wpt/tests/interfaces/mediasession.idl +++ b/tests/wpt/tests/interfaces/mediasession.idl @@ -56,6 +56,7 @@ interface MediaMetadata { attribute DOMString artist; attribute DOMString album; attribute FrozenArray<MediaImage> artwork; + [SameObject] readonly attribute FrozenArray<ChapterInformation> chapterInfo; }; dictionary MediaMetadataInit { @@ -63,6 +64,20 @@ dictionary MediaMetadataInit { DOMString artist = ""; DOMString album = ""; sequence<MediaImage> artwork = []; + sequence<ChapterInformationInit> chapterInfo = []; +}; + +[Exposed=Window] +interface ChapterInformation { + readonly attribute DOMString title; + readonly attribute double startTime; + [SameObject] readonly attribute FrozenArray<MediaImage> artwork; +}; + +dictionary ChapterInformationInit { + DOMString title = ""; + double startTime = 0; + sequence<MediaImage> artwork = []; }; dictionary MediaImage { diff --git a/tests/wpt/tests/interfaces/sanitizer-api.idl b/tests/wpt/tests/interfaces/sanitizer-api.idl index 599d8f82ea4..8f5c667973a 100644 --- a/tests/wpt/tests/interfaces/sanitizer-api.idl +++ b/tests/wpt/tests/interfaces/sanitizer-api.idl @@ -3,19 +3,15 @@ // (https://github.com/w3c/webref) // Source: HTML Sanitizer API (https://wicg.github.io/sanitizer-api/) -partial interface Element { - [CEReactions] undefined setHTMLUnsafe__TO_BE_MERGED(DOMString html, optional SanitizerConfig config = {}); - [CEReactions] undefined setHTML(DOMString html, optional SanitizerConfig config = {}); +dictionary SetHTMLOptions { + (Sanitizer or SanitizerConfig) sanitizer = {}; }; -partial interface ShadowRoot { - [CEReactions] undefined setHTMLUnsafe__TO_BE_MERGED(DOMString html, optional SanitizerConfig config = {}); - [CEReactions] undefined setHTML(DOMString html, optional SanitizerConfig config = {}); -}; - -partial interface Document { - static Document parseHTMLUnsafe__TO_BE_MERGED(DOMString html, optional SanitizerConfig config = {}); - static Document parseHTML(DOMString html, optional SanitizerConfig config = {}); +[Exposed=(Window,Worker)] +interface Sanitizer { + constructor(optional SanitizerConfig config = {}); + SanitizerConfig get(); + SanitizerConfig getUnsafe(); }; dictionary SanitizerElementNamespace { diff --git a/tests/wpt/tests/interfaces/service-workers.idl b/tests/wpt/tests/interfaces/service-workers.idl index c740e1098a8..1ddc6d71d83 100644 --- a/tests/wpt/tests/interfaces/service-workers.idl +++ b/tests/wpt/tests/interfaces/service-workers.idl @@ -183,6 +183,7 @@ dictionary RouterCondition { RunningStatus runningStatus; sequence<RouterCondition> _or; + RouterCondition not; }; typedef (RouterSourceDict or RouterSourceEnum) RouterSource; diff --git a/tests/wpt/tests/interfaces/shape-detection-api.idl b/tests/wpt/tests/interfaces/shape-detection-api.idl index 4fc1f085ea2..24d3b980854 100644 --- a/tests/wpt/tests/interfaces/shape-detection-api.idl +++ b/tests/wpt/tests/interfaces/shape-detection-api.idl @@ -17,11 +17,11 @@ dictionary FaceDetectorOptions { dictionary DetectedFace { required DOMRectReadOnly boundingBox; - required FrozenArray<Landmark>? landmarks; + required sequence<Landmark>? landmarks; }; dictionary Landmark { - required FrozenArray<Point2D> locations; + required sequence<Point2D> locations; LandmarkType type; }; @@ -48,7 +48,7 @@ dictionary DetectedBarcode { required DOMRectReadOnly boundingBox; required DOMString rawValue; required BarcodeFormat format; - required FrozenArray<Point2D> cornerPoints; + required sequence<Point2D> cornerPoints; }; enum BarcodeFormat { diff --git a/tests/wpt/tests/interfaces/shared-storage.idl b/tests/wpt/tests/interfaces/shared-storage.idl index edbe2c2bcc3..c40344e74d2 100644 --- a/tests/wpt/tests/interfaces/shared-storage.idl +++ b/tests/wpt/tests/interfaces/shared-storage.idl @@ -3,40 +3,30 @@ // (https://github.com/w3c/webref) // Source: Shared Storage API (https://wicg.github.io/shared-storage/) +typedef (USVString or FencedFrameConfig) SharedStorageResponse; + [Exposed=(Window)] interface SharedStorageWorklet : Worklet { + Promise<SharedStorageResponse> selectURL(DOMString name, + FrozenArray<SharedStorageUrlWithMetadata> urls, + optional SharedStorageRunOperationMethodOptions options = {}); + Promise<any> run(DOMString name, + optional SharedStorageRunOperationMethodOptions options = {}); }; +callback RunFunctionForSharedStorageSelectURLOperation = Promise<unsigned long>(sequence<USVString> urls, optional any data); + [Exposed=SharedStorageWorklet, Global=SharedStorageWorklet] interface SharedStorageWorkletGlobalScope : WorkletGlobalScope { undefined register(DOMString name, - SharedStorageOperationConstructor operationCtor); + Function operationCtor); readonly attribute WorkletSharedStorage sharedStorage; }; -callback SharedStorageOperationConstructor = - SharedStorageOperation(optional SharedStorageRunOperationMethodOptions options); - -[Exposed=SharedStorageWorklet] -interface SharedStorageOperation { -}; - -dictionary SharedStorageRunOperationMethodOptions { - object data; - boolean resolveToConfig = false; - boolean keepAlive = false; -}; - -[Exposed=SharedStorageWorklet] -interface SharedStorageRunOperation : SharedStorageOperation { - Promise<undefined> run(object data); -}; - -[Exposed=SharedStorageWorklet] -interface SharedStorageSelectURLOperation : SharedStorageOperation { - Promise<long> run(object data, - FrozenArray<SharedStorageUrlWithMetadata> urls); +dictionary SharedStorageUrlWithMetadata { + required USVString url; + object reportingMetadata; }; [Exposed=(Window,SharedStorageWorklet)] @@ -54,22 +44,23 @@ dictionary SharedStorageSetMethodOptions { boolean ignoreIfPresent = false; }; -typedef (USVString or FencedFrameConfig) SharedStorageResponse; - [Exposed=(Window)] interface WindowSharedStorage : SharedStorage { - Promise<any> run(DOMString name, - optional SharedStorageRunOperationMethodOptions options = {}); Promise<SharedStorageResponse> selectURL(DOMString name, FrozenArray<SharedStorageUrlWithMetadata> urls, optional SharedStorageRunOperationMethodOptions options = {}); + Promise<any> run(DOMString name, + optional SharedStorageRunOperationMethodOptions options = {}); + + Promise<SharedStorageWorklet> createWorklet(USVString moduleURL, optional WorkletOptions options = {}); readonly attribute SharedStorageWorklet worklet; }; -dictionary SharedStorageUrlWithMetadata { - required USVString url; - object reportingMetadata; +dictionary SharedStorageRunOperationMethodOptions { + object data; + boolean resolveToConfig = false; + boolean keepAlive = false; }; partial interface Window { diff --git a/tests/wpt/tests/interfaces/text-detection-api.idl b/tests/wpt/tests/interfaces/text-detection-api.idl index 95b642749f7..b6745b18754 100644 --- a/tests/wpt/tests/interfaces/text-detection-api.idl +++ b/tests/wpt/tests/interfaces/text-detection-api.idl @@ -14,5 +14,5 @@ dictionary DetectedText { required DOMRectReadOnly boundingBox; required DOMString rawValue; - required FrozenArray<Point2D> cornerPoints; + required sequence<Point2D> cornerPoints; }; diff --git a/tests/wpt/tests/interfaces/trusted-types.idl b/tests/wpt/tests/interfaces/trusted-types.idl index db5bd635cf9..a0f88e4e6c3 100644 --- a/tests/wpt/tests/interfaces/trusted-types.idl +++ b/tests/wpt/tests/interfaces/trusted-types.idl @@ -32,12 +32,12 @@ interface TrustedScriptURL { DOMString? getAttributeType( DOMString tagName, DOMString attribute, - optional DOMString elementNs = "", - optional DOMString attrNs = ""); + optional DOMString? elementNs = "", + optional DOMString? attrNs = ""); DOMString? getPropertyType( DOMString tagName, DOMString property, - optional DOMString elementNs = ""); + optional DOMString? elementNs = ""); readonly attribute TrustedTypePolicy? defaultPolicy; }; diff --git a/tests/wpt/tests/interfaces/turtledove.idl b/tests/wpt/tests/interfaces/turtledove.idl index 2547e1fb549..39e90ddae19 100644 --- a/tests/wpt/tests/interfaces/turtledove.idl +++ b/tests/wpt/tests/interfaces/turtledove.idl @@ -82,6 +82,7 @@ dictionary AuctionAdConfig { Promise<record<USVString, any>> perBuyerSignals; Promise<record<USVString, unsigned long long>> perBuyerTimeouts; Promise<record<USVString, unsigned long long>> perBuyerCumulativeTimeouts; + unsigned long long reportingTimeout; USVString sellerCurrency; Promise<record<USVString, USVString>> perBuyerCurrencies; record<USVString, unsigned short> perBuyerGroupLimits; diff --git a/tests/wpt/tests/interfaces/wasm-js-api.idl b/tests/wpt/tests/interfaces/wasm-js-api.idl index 0d4384251df..b4f723d050a 100644 --- a/tests/wpt/tests/interfaces/wasm-js-api.idl +++ b/tests/wpt/tests/interfaces/wasm-js-api.idl @@ -62,6 +62,8 @@ dictionary MemoryDescriptor { interface Memory { constructor(MemoryDescriptor descriptor); unsigned long grow([EnforceRange] unsigned long delta); + ArrayBuffer toFixedLengthBuffer(); + ArrayBuffer toResizableBuffer(); readonly attribute ArrayBuffer buffer; }; diff --git a/tests/wpt/tests/interfaces/webcodecs.idl b/tests/wpt/tests/interfaces/webcodecs.idl index 371546eb0d0..19964c51e45 100644 --- a/tests/wpt/tests/interfaces/webcodecs.idl +++ b/tests/wpt/tests/interfaces/webcodecs.idl @@ -371,6 +371,8 @@ dictionary VideoFrameBufferInit { VideoColorSpaceInit colorSpace; sequence<ArrayBuffer> transfer = []; + + VideoFrameMetadata metadata; }; dictionary VideoFrameMetadata { diff --git a/tests/wpt/tests/interfaces/webidl.idl b/tests/wpt/tests/interfaces/webidl.idl index dff46c557cb..f3db91096ac 100644 --- a/tests/wpt/tests/interfaces/webidl.idl +++ b/tests/wpt/tests/interfaces/webidl.idl @@ -6,7 +6,7 @@ typedef (Int8Array or Int16Array or Int32Array or Uint8Array or Uint16Array or Uint32Array or Uint8ClampedArray or BigInt64Array or BigUint64Array or - Float32Array or Float64Array or DataView) ArrayBufferView; + Float16Array or Float32Array or Float64Array or DataView) ArrayBufferView; typedef (ArrayBufferView or ArrayBuffer) BufferSource; typedef (ArrayBuffer or SharedArrayBuffer or [AllowShared] ArrayBufferView) AllowSharedBufferSource; diff --git a/tests/wpt/tests/interfaces/webnn.idl b/tests/wpt/tests/interfaces/webnn.idl index 0b8ea7cb344..9beb8858d10 100644 --- a/tests/wpt/tests/interfaces/webnn.idl +++ b/tests/wpt/tests/interfaces/webnn.idl @@ -64,11 +64,7 @@ enum MLOperandDataType { }; dictionary MLOperandDescriptor { - // The operand type. required MLOperandDataType dataType; - - // The dimensions field is empty for scalar operands, - // and non-empty for tensor operands. sequence<[EnforceRange] unsigned long> dimensions = []; }; @@ -122,7 +118,7 @@ dictionary MLBatchNormalizationOptions { partial interface MLGraphBuilder { MLOperand batchNormalization(MLOperand input, MLOperand mean, MLOperand variance, - optional MLBatchNormalizationOptions options = {}); + optional MLBatchNormalizationOptions options = {}); }; partial interface MLGraphBuilder { @@ -162,7 +158,9 @@ dictionary MLConv2dOptions { }; partial interface MLGraphBuilder { - MLOperand conv2d(MLOperand input, MLOperand filter, optional MLConv2dOptions options = {}); + MLOperand conv2d(MLOperand input, + MLOperand filter, + optional MLConv2dOptions options = {}); }; enum MLConvTranspose2dFilterOperandLayout { @@ -242,7 +240,14 @@ dictionary MLGatherOptions { }; partial interface MLGraphBuilder { - MLOperand gather(MLOperand input, MLOperand indices, optional MLGatherOptions options = {}); + MLOperand gather(MLOperand input, + MLOperand indices, + optional MLGatherOptions options = {}); +}; + +partial interface MLGraphBuilder { + MLOperand gelu(MLOperand input); + MLActivation gelu(); }; dictionary MLGemmOptions { @@ -329,7 +334,7 @@ dictionary MLInstanceNormalizationOptions { partial interface MLGraphBuilder { MLOperand instanceNormalization(MLOperand input, - optional MLInstanceNormalizationOptions options = {}); + optional MLInstanceNormalizationOptions options = {}); }; dictionary MLLayerNormalizationOptions { @@ -340,7 +345,8 @@ dictionary MLLayerNormalizationOptions { }; partial interface MLGraphBuilder { - MLOperand layerNormalization(MLOperand input, optional MLLayerNormalizationOptions options = {}); + MLOperand layerNormalization(MLOperand input, + optional MLLayerNormalizationOptions options = {}); }; dictionary MLLeakyReluOptions { @@ -532,9 +538,10 @@ dictionary MLSplitOptions { }; partial interface MLGraphBuilder { - sequence<MLOperand> split(MLOperand input, - ([EnforceRange] unsigned long or sequence<[EnforceRange] unsigned long>) splits, - optional MLSplitOptions options = {}); + sequence<MLOperand> split( + MLOperand input, + ([EnforceRange] unsigned long or sequence<[EnforceRange] unsigned long>) splits, + optional MLSplitOptions options = {}); }; partial interface MLGraphBuilder { diff --git a/tests/wpt/tests/interfaces/webrtc-encoded-transform.idl b/tests/wpt/tests/interfaces/webrtc-encoded-transform.idl index 8a756702c7e..0db2f2b9a81 100644 --- a/tests/wpt/tests/interfaces/webrtc-encoded-transform.idl +++ b/tests/wpt/tests/interfaces/webrtc-encoded-transform.idl @@ -78,10 +78,15 @@ dictionary RTCEncodedVideoFrameMetadata { DOMString mimeType; }; +dictionary RTCEncodedVideoFrameOptions { + RTCEncodedVideoFrameMetadata metadata; +}; + // New interfaces to define encoded video and audio frames. Will eventually // re-use or extend the equivalent defined in WebCodecs. [Exposed=(Window,DedicatedWorker), Serializable] interface RTCEncodedVideoFrame { + constructor(RTCEncodedVideoFrame originalFrame, optional RTCEncodedVideoFrameOptions options = {}); readonly attribute RTCEncodedVideoFrameType type; attribute ArrayBuffer data; RTCEncodedVideoFrameMetadata getMetadata(); @@ -96,8 +101,13 @@ dictionary RTCEncodedAudioFrameMetadata { DOMString mimeType; }; +dictionary RTCEncodedAudioFrameOptions { + RTCEncodedAudioFrameMetadata metadata; +}; + [Exposed=(Window,DedicatedWorker), Serializable] interface RTCEncodedAudioFrame { + constructor(RTCEncodedAudioFrame originalFrame, optional RTCEncodedAudioFrameOptions options = {}); attribute ArrayBuffer data; RTCEncodedAudioFrameMetadata getMetadata(); }; diff --git a/tests/wpt/tests/interfaces/webrtc.idl b/tests/wpt/tests/interfaces/webrtc.idl index e571abb527f..65e7aa622c5 100644 --- a/tests/wpt/tests/interfaces/webrtc.idl +++ b/tests/wpt/tests/interfaces/webrtc.idl @@ -368,6 +368,7 @@ interface RTCRtpReceiver { sequence<RTCRtpContributingSource> getContributingSources(); sequence<RTCRtpSynchronizationSource> getSynchronizationSources(); Promise<RTCStatsReport> getStats(); + attribute DOMHighResTimeStamp? jitterBufferTarget; }; dictionary RTCRtpContributingSource { @@ -387,7 +388,7 @@ interface RTCRtpTransceiver { attribute RTCRtpTransceiverDirection direction; readonly attribute RTCRtpTransceiverDirection? currentDirection; undefined stop(); - undefined setCodecPreferences(sequence<RTCRtpCodecCapability> codecs); + undefined setCodecPreferences(sequence<RTCRtpCodec> codecs); }; [Exposed=Window] @@ -434,8 +435,8 @@ dictionary RTCIceParameters { }; dictionary RTCIceCandidatePair { - RTCIceCandidate local; - RTCIceCandidate remote; + required RTCIceCandidate local; + required RTCIceCandidate remote; }; enum RTCIceGathererState { diff --git a/tests/wpt/tests/interfaces/webxr.idl b/tests/wpt/tests/interfaces/webxr.idl index 3b7f8a55b7c..8e02fbd38a0 100644 --- a/tests/wpt/tests/interfaces/webxr.idl +++ b/tests/wpt/tests/interfaces/webxr.idl @@ -178,6 +178,7 @@ interface XRInputSource { [SameObject] readonly attribute XRSpace targetRaySpace; [SameObject] readonly attribute XRSpace? gripSpace; [SameObject] readonly attribute FrozenArray<DOMString> profiles; + [SameObject] readonly attribute boolean skipRendering; }; [SecureContext, Exposed=Window] @@ -263,8 +264,8 @@ interface XRInputSourcesChangeEvent : Event { dictionary XRInputSourcesChangeEventInit : EventInit { required XRSession session; - required FrozenArray<XRInputSource> added; - required FrozenArray<XRInputSource> removed; + required sequence<XRInputSource> added; + required sequence<XRInputSource> removed; }; diff --git a/tests/wpt/tests/long-animation-frame/tentative/loaf-stream-source-location.html b/tests/wpt/tests/long-animation-frame/tentative/loaf-stream-source-location.html index 5776ff52552..0fd30859d73 100644 --- a/tests/wpt/tests/long-animation-frame/tentative/loaf-stream-source-location.html +++ b/tests/wpt/tests/long-animation-frame/tentative/loaf-stream-source-location.html @@ -17,8 +17,7 @@ promise_test(async t => { const scriptElement = document.createElement("script"); scriptElement.src = scriptLocation; document.body.appendChild(scriptElement); - }, script => { - return script.invoker === "Promise.resolve" }, t); + }, script => script.invoker === "StreamPromise.resolve.then", t); assert_true(script.sourceURL.includes("stream-promise-generates-loaf.js")); }, "Source location should be extracted for stream promises"); diff --git a/tests/wpt/tests/long-animation-frame/tentative/loaf-stream.html b/tests/wpt/tests/long-animation-frame/tentative/loaf-stream.html index 424f2cd0d1e..e35bc2f9aa7 100644 --- a/tests/wpt/tests/long-animation-frame/tentative/loaf-stream.html +++ b/tests/wpt/tests/long-animation-frame/tentative/loaf-stream.html @@ -34,6 +34,6 @@ test_promise_script(async t => { }); response.body.pipeTo(writable); await readable.getReader().read(); -}, "resolve", "Promise.resolve"); +}, "resolve", "StreamPromise.resolve"); </script> </body> diff --git a/tests/wpt/tests/mediacapture-record/MediaRecorder-canvas-media-source.https.html b/tests/wpt/tests/mediacapture-record/MediaRecorder-canvas-media-source.https.html index 0680c218795..e640714d5cf 100644 --- a/tests/wpt/tests/mediacapture-record/MediaRecorder-canvas-media-source.https.html +++ b/tests/wpt/tests/mediacapture-record/MediaRecorder-canvas-media-source.https.html @@ -9,8 +9,6 @@ <meta name=variant content="?mimeType=video/webm;codecs=vp9,opus"> <meta name=variant content="?mimeType=video/webm;codecs=av1,opus"> <meta name=variant content="?mimeType=video/mp4;codecs=avc1,mp4a.40.2"> - <meta name=variant content="?mimeType=video/mp4;codecs=vp9,opus"> - <meta name=variant content="?mimeType=video/mp4"> <link rel="help" href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#dom-mediarecorder-mimeType"> <script src="/resources/testharness.js"></script> @@ -86,9 +84,9 @@ async_test(test => { const params = new URLSearchParams(window.location.search); const mimeType = params.get('mimeType'); - if (mimeType && !MediaRecorder.isTypeSupported(mimeType)) { - test.done(); - return; + if (mimeType) { + assert_implements_optional(MediaRecorder.isTypeSupported(mimeType), + `"${mimeType}" for MediaRecorder is not supported`); } const canvas = document.querySelector("canvas"); diff --git a/tests/wpt/tests/mediacapture-record/MediaRecorder-events-and-exceptions.html b/tests/wpt/tests/mediacapture-record/MediaRecorder-events-and-exceptions.html index 97ada212663..409e46c91da 100644 --- a/tests/wpt/tests/mediacapture-record/MediaRecorder-events-and-exceptions.html +++ b/tests/wpt/tests/mediacapture-record/MediaRecorder-events-and-exceptions.html @@ -8,8 +8,6 @@ <meta name=variant content="?mimeType=video/webm;codecs=vp9,opus"> <meta name=variant content="?mimeType=video/webm;codecs=av1,opus"> <meta name=variant content="?mimeType=video/mp4;codecs=avc1,mp4a.40.2"> - <meta name=variant content="?mimeType=video/mp4;codecs=vp9,opus"> - <meta name=variant content="?mimeType=video/mp4"> <link rel="help" href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#mediarecorder"> <script src="/resources/testharness.js"></script> <script src="/resources/testharnessreport.js"></script> @@ -83,9 +81,9 @@ const params = new URLSearchParams(window.location.search); const mimeType = params.get('mimeType'); - if (mimeType && !MediaRecorder.isTypeSupported(mimeType)) { - test.done(); - return; + if (mimeType) { + assert_implements_optional(MediaRecorder.isTypeSupported(mimeType), + `"${mimeType}" for MediaRecorder is not supported`); } const recorder = new MediaRecorder(new MediaStream(), { mimeType }); diff --git a/tests/wpt/tests/mediacapture-record/MediaRecorder-mimetype.html b/tests/wpt/tests/mediacapture-record/MediaRecorder-mimetype.html index 57baa1346f1..74248d65f46 100644 --- a/tests/wpt/tests/mediacapture-record/MediaRecorder-mimetype.html +++ b/tests/wpt/tests/mediacapture-record/MediaRecorder-mimetype.html @@ -21,7 +21,6 @@ const AUDIO_CODECS_MIME_TYPES = [ 'audio/webm; codecs="vorbis"', 'audio/webm; codecs="opus"', 'audio/mp4: codecs="mp4a.40.2"', - 'audio/mp4: codecs="opus"', ]; const VIDEO_ONLY_MIME_TYPES = [ @@ -34,7 +33,6 @@ const VIDEO_CODECS_MIME_TYPES = [ 'video/webm; codecs="vp9"', 'video/webm; codecs="av1"', 'video/mp4: codecs="avc1"', - 'video/mp4: codecs="vp9"', ]; const AUDIO_VIDEO_MIME_TYPES = [ @@ -44,7 +42,6 @@ const AUDIO_VIDEO_MIME_TYPES = [ 'video/webm; codecs="vp9, opus"', 'video/webm; codecs="av1, opus"', 'video/mp4: codecs="avc1, mp4a.40.2"', - 'video/mp4; codecs="vp9, opus"', ]; const AUDIO_MIME_TYPES = [ diff --git a/tests/wpt/tests/mediacapture-record/MediaRecorder-pause-resume.html b/tests/wpt/tests/mediacapture-record/MediaRecorder-pause-resume.html index f584508a0d0..8dc231279a0 100644 --- a/tests/wpt/tests/mediacapture-record/MediaRecorder-pause-resume.html +++ b/tests/wpt/tests/mediacapture-record/MediaRecorder-pause-resume.html @@ -8,10 +8,6 @@ <meta name=variant content="?mimeType=video/webm;codecs=vp9,opus"> <meta name=variant content="?mimeType=video/webm;codecs=av1,opus"> <meta name=variant content="?mimeType=video/mp4;codecs=avc1,mp4a.40.2"> - <meta name=variant content="?mimeType=video/mp4;codecs=avc1,opus"> - <meta name=variant content="?mimeType=video/mp4;codecs=vp9,opus"> - <meta name=variant content="?mimeType=video/mp4;codecs=vp9,mp4a.40.2"> - <meta name=variant content="?mimeType=video/mp4"> <link rel="help" href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#mediarecorder"> <script src="/resources/testharness.js"></script> <script src="/resources/testharnessreport.js"></script> diff --git a/tests/wpt/tests/mediacapture-record/MediaRecorder-peerconnection.https.html b/tests/wpt/tests/mediacapture-record/MediaRecorder-peerconnection.https.html index daae044fa8d..3fbc1f0f2d2 100644 --- a/tests/wpt/tests/mediacapture-record/MediaRecorder-peerconnection.https.html +++ b/tests/wpt/tests/mediacapture-record/MediaRecorder-peerconnection.https.html @@ -4,20 +4,6 @@ <head> <title>MediaRecorder peer connection</title> - <meta name=variant content="?kinds=video&mimeType=''"> - <meta name=variant content="?kinds=audio&mimeType=''"> - <meta name=variant content="?kinds=video,audio&mimeType=''"> - <meta name=variant content="?kinds=audio&mimeType=audio/webm;codecs=opus"> - <meta name=variant content="?kinds=video&mimeType=video/webm;codecs=vp8"> - <meta name=variant content="?kinds=video,audio&mimeType=video/webm;codecs=vp8,opus"> - <meta name=variant content="?kinds=video&mimeType=video/webm;codecs=vp9"> - <meta name=variant content="?kinds=video,audio&mimeType=video/webm;codecs=vp9,opus"> - <meta name=variant content="?kinds=video,audio&mimeType=video/mp4;codecs=avc1,mp4a.40.2"> - <meta name=variant content="?kinds=video&mimeType=video/mp4;codecs=vp9"> - <meta name=variant content="?kinds=audio&mimeType=audio/mp4;codecs=opus"> - <meta name=variant content="?kinds=video,audio&mimeType=video/mp4;codecs=vp9,opus"> - <meta name=variant content="?kinds=video,audio&mimeType=video/mp4"> - <link rel="help" href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#dom-mediarecorder-mimeType"> <script src="/resources/testharness.js"></script> @@ -29,83 +15,74 @@ </head> <body> -<video id="remote" autoplay width="240"></video> - -<script> - const params = new URLSearchParams(window.location.search); - const mimeType = params.get('mimeType'); - const kinds = params.get('kinds'); - const tag = `kinds "${kinds} "mimeType "${mimeType}"`; - let stream; - let pc; + <video id="remote" autoplay width="240"></video> + <script> - promise_setup(async () => { - const t = {add_cleanup: add_completion_callback}; - const [, connection_pc, connection_stream] = await startConnection(t, true, true); - pc = connection_pc; - - let video = null; - if (kinds.indexOf('video') != -1) { - video = connection_stream.getVideoTracks()[0]; - } +promise_setup(async () => { + const t = {add_cleanup: add_completion_callback}; + const [, pc, stream] = await startConnection(t, true, true); + const [audio] = stream.getAudioTracks(); + const [video] = stream.getVideoTracks(); - let audio = null; - if (kinds.indexOf('audio') != -1) { - audio = connection_stream.getAudioTracks()[0]; - } + // Needed for the tests to get exercised in Chrome (bug) + document.getElementById('remote').srcObject = stream; - // Needed for the tests to get exercised in Chrome (bug) - document.getElementById('remote').srcObject = connection_stream; - stream = new MediaStream([audio, video].filter(n => n)); - }); + for (const {kinds, mimeType} of [ + { kinds: { video }, mimeType: "" }, + { kinds: { audio }, mimeType: "" }, + { kinds: { video, audio }, mimeType: "" }, + { kinds: { audio }, mimeType: "audio/webm;codecs=opus" }, + { kinds: { video }, mimeType: "video/webm;codecs=vp8" }, + { kinds: { video, audio }, mimeType: "video/webm;codecs=vp8,opus" }, + { kinds: { video }, mimeType: "video/webm;codecs=vp9" }, + { kinds: { video, audio }, mimeType: "video/webm;codecs=vp9,opus" }, + { kinds: { audio }, mimeType: "audio/mp4;codecs=mp4a.40.2" }, + { kinds: { video, audio }, mimeType: "video/mp4;codecs=avc1,mp4a.40.2" } + ]) { + const tag = `${JSON.stringify(kinds)} mimeType "${mimeType}"`; + const stream = new MediaStream([kinds.audio, kinds.video].filter(n => n)); - promise_test(async t => { // Spec doesn't mandate codecs, so if not supported, test failure instead. if (mimeType && !MediaRecorder.isTypeSupported(mimeType)) { promise_test(async t => { assert_throws_dom('NotSupportedError', () => new MediaRecorder(stream, { mimeType })); - }, `MediaRecorder constructor throws on no support 1, ${tag}`); - return; + }, `MediaRecorder constructor throws on no support, ${tag}`); + continue; } - const recorder = new MediaRecorder(stream, { mimeType }); - recorder.start(200); - await new Promise(r => recorder.onstart = r); - let combinedSize = 0; - // Wait for a small amount of data to appear. Kept small for mobile tests - while (combinedSize < 2000) { - const {data} = await new Promise(r => recorder.ondataavailable = r); - combinedSize += data.size; - } - recorder.stop(); - }, `PeerConnection MediaRecorder receives data after onstart, ${tag}`); + promise_test(async t => { + const recorder = new MediaRecorder(stream, { mimeType }); + recorder.start(200); + await new Promise(r => recorder.onstart = r); + let combinedSize = 0; + // Wait for a small amount of data to appear. Kept small for mobile tests + while (combinedSize < 2000) { + const {data} = await new Promise(r => recorder.ondataavailable = r); + combinedSize += data.size; + } + recorder.stop(); + }, `PeerConnection MediaRecorder receives data after onstart, ${tag}`); - promise_test(async t => { - // Spec doesn't mandate codecs, so if not supported, test failure instead. - if (mimeType && !MediaRecorder.isTypeSupported(mimeType)) { - promise_test(async t => { - assert_throws_dom('NotSupportedError', - () => new MediaRecorder(stream, { mimeType })); - }, `MediaRecorder constructor throws on no support 2, ${tag}`); - return; - } - - const clone = stream.clone(); - const recorder = new MediaRecorder(clone, { mimeType }); - recorder.start(); - await new Promise(r => recorder.onstart = r); - await waitForReceivedFramesOrPackets(t, pc, kinds.audio, kinds.video, 10); - for (const track of clone.getTracks()) { - track.stop(); - } - // As the tracks ended, expect data from the recorder. - await Promise.all([ - new Promise(r => recorder.onstop = r), - new Promise(r => recorder.ondataavailable = r) - ]); - }, `PeerConnection MediaRecorder gets ondata on stopping tracks, ${tag}`); + promise_test(async t => { + const clone = stream.clone(); + const recorder = new MediaRecorder(clone, { mimeType }); + recorder.start(); + await new Promise(r => recorder.onstart = r); + await waitForReceivedFramesOrPackets(t, pc, kinds.audio, kinds.video, 10); + for (const track of clone.getTracks()) { + track.stop(); + } + // As the tracks ended, expect data from the recorder. + await Promise.all([ + new Promise(r => recorder.onstop = r), + new Promise(r => recorder.ondataavailable = r) + ]); + }, `PeerConnection MediaRecorder gets ondata on stopping tracks, ${tag}`); + } +}); </script> </body> + </html> diff --git a/tests/wpt/tests/mediacapture-record/MediaRecorder-stop.html b/tests/wpt/tests/mediacapture-record/MediaRecorder-stop.html index 9ef50516381..d6ce3707720 100644 --- a/tests/wpt/tests/mediacapture-record/MediaRecorder-stop.html +++ b/tests/wpt/tests/mediacapture-record/MediaRecorder-stop.html @@ -7,8 +7,6 @@ <meta name=variant content="?mimeType=video/webm;codecs=vp9,opus"> <meta name=variant content="?mimeType=video/webm;codecs=av1,opus"> <meta name=variant content="?mimeType=video/mp4;codecs=avc1,mp4a.40.2"> - <meta name=variant content="?mimeType=video/mp4;codecs=vp9,opus"> - <meta name=variant content="?mimeType=video/mp4"> <link rel="help" href="https://w3c.github.io/mediacapture-record/MediaRecorder.html#mediarecorder"> <script src="/resources/testharness.js"></script> <script src="/resources/testharnessreport.js"></script> @@ -40,21 +38,18 @@ return true; } - function isMimetypeSupported(mimeType, t) { - if (mimeType && !MediaRecorder.isTypeSupported(mimeType)) { - t.done(); - return false; + function doneWithUnsupportedType(mimeType) { + if (mimeType) { + assert_implements_optional(MediaRecorder.isTypeSupported(mimeType), + `"${mimeType}" for MediaRecorder is not supported`); } - return true; } const params = new URLSearchParams(window.location.search); const mimeType = params.get('mimeType'); const tag = `mimeType "${mimeType}"`; promise_test(async t => { - if (!isMimetypeSupported(mimeType, t)) { - return; - } + doneWithUnsupportedType(mimeType); const {stream: video} = createVideoStream(t); const recorder = new MediaRecorder(video, {mimeType}); @@ -78,9 +73,7 @@ }, "MediaRecorder will stop recording and fire a stop event when all tracks are ended"); promise_test(async t => { - if (!isMimetypeSupported(mimeType, t)) { - return; - } + doneWithUnsupportedType(mimeType); const {stream: video} = createVideoStream(t); const recorder = new MediaRecorder(video, {mimeType}); @@ -103,9 +96,7 @@ }, "MediaRecorder will stop recording and fire a stop event when stop() is called"); promise_test(async t => { - if (!isMimetypeSupported(mimeType, t)) { - return; - } + doneWithUnsupportedType(mimeType); const recorder = new MediaRecorder(createVideoStream(t).stream, {mimeType}); recorder.stop(); @@ -116,9 +107,7 @@ }, "MediaRecorder will not fire an exception when stopped after creation"); promise_test(async t => { - if (!isMimetypeSupported(mimeType, t)) { - return; - } + doneWithUnsupportedType(mimeType); const recorder = new MediaRecorder(createVideoStream(t).stream, {mimeType}); recorder.start(); @@ -132,9 +121,7 @@ }, "MediaRecorder will not fire an exception when stopped after having just been stopped"); promise_test(async t => { - if (!isMimetypeSupported(mimeType, t)) { - return; - } + doneWithUnsupportedType(mimeType); const {stream} = createVideoStream(t); const recorder = new MediaRecorder(stream, {mimeType}); @@ -149,9 +136,7 @@ }, "MediaRecorder will not fire an exception when stopped after having just been spontaneously stopped"); promise_test(async t => { - if (!isMimetypeSupported(mimeType, t)) { - return; - } + doneWithUnsupportedType(mimeType); const {stream} = createAudioVideoStream(t); const recorder = new MediaRecorder(stream, {mimeType}); @@ -170,9 +155,7 @@ }, "MediaRecorder will fire start event even if stopped synchronously"); promise_test(async t => { - if (!isMimetypeSupported(mimeType, t)) { - return; - } + doneWithUnsupportedType(mimeType); const {stream} = createAudioVideoStream(t); const recorder = new MediaRecorder(stream, {mimeType}); @@ -195,9 +178,7 @@ }, "MediaRecorder will fire start event even if a track is removed synchronously"); promise_test(async t => { - if (!isMimetypeSupported(mimeType, t)) { - return; - } + doneWithUnsupportedType(mimeType); const {stream} = createFlowingAudioVideoStream(t); const recorder = new MediaRecorder(stream, {mimeType}); diff --git a/tests/wpt/tests/pointerevents/deviceproperties/get-device-properties-uniqueid-from-pointer-event.tentative.html b/tests/wpt/tests/pointerevents/deviceproperties/get-device-properties-uniqueid-from-pointer-event.tentative.html index 53b4d2c8d55..dc6b9379c11 100644 --- a/tests/wpt/tests/pointerevents/deviceproperties/get-device-properties-uniqueid-from-pointer-event.tentative.html +++ b/tests/wpt/tests/pointerevents/deviceproperties/get-device-properties-uniqueid-from-pointer-event.tentative.html @@ -16,12 +16,13 @@ </style> <div id="logger" draggable="false"></div> <div id="console"></div> -<!-- This test verifies that if the PointerEventDeviceId flag is enabled, - pointerEvent.deviceProperties.uniqueId is -1. If not, it is undefined. --> +<!-- This test verifies that pointerEvent.deviceProperties.uniqueId is 0 + by default for a pointer with an invalid hardware id - in this case + a testdriver generated event, which does not support hardware id. --> <script> function CheckDeviceId(event) { eventFired++; - assert_equals(event.deviceProperties.uniqueId, -1, "deviceId is -1"); + assert_equals(event.deviceProperties.uniqueId, 0, "deviceId is 0"); } window.addEventListener("pointerdown", CheckDeviceId, false); diff --git a/tests/wpt/tests/pointerevents/deviceproperties/pointer-event-has-device-properties-uniqueid-from-pointer-event-init.tentative.html b/tests/wpt/tests/pointerevents/deviceproperties/pointer-event-has-device-properties-uniqueid-from-pointer-event-init.tentative.html index 029edef7694..a37df4b4214 100644 --- a/tests/wpt/tests/pointerevents/deviceproperties/pointer-event-has-device-properties-uniqueid-from-pointer-event-init.tentative.html +++ b/tests/wpt/tests/pointerevents/deviceproperties/pointer-event-has-device-properties-uniqueid-from-pointer-event-init.tentative.html @@ -7,12 +7,11 @@ <script src="/resources/testharnessreport.js"></script> <div id="console"></div> -<!-- This test verifies that if the kPointerEventDeviceId flag is enabled, - pointerEvent.deviceProperties.uniqueId can be set via PointerEventInit. If not, it is - undefined. --> +<!-- This test verifies that pointerEvent.deviceProperties.uniqueId + can be set via PointerEventInit. --> <script> const UNIQUE_ID = 1001; - const INVALID_UNIQUE_ID = -1; + const INVALID_UNIQUE_ID = 0; function CheckDeviceId(event, uniqueId) { assert_equals(event.deviceProperties.uniqueId, uniqueId, "uniqueId is populated"); diff --git a/tests/wpt/tests/screen-details/META.yml b/tests/wpt/tests/screen-details/META.yml index abfee89ffbd..2e3284c67cc 100644 --- a/tests/wpt/tests/screen-details/META.yml +++ b/tests/wpt/tests/screen-details/META.yml @@ -1 +1 @@ -spec: https://webscreens.github.io/window-placement/ +spec: https://webscreens.github.io/window-management/ diff --git a/tests/wpt/tests/scroll-animations/css/animation-shorthand.html b/tests/wpt/tests/scroll-animations/css/animation-shorthand.html index b7d5947a212..cb63137f5c9 100644 --- a/tests/wpt/tests/scroll-animations/css/animation-shorthand.html +++ b/tests/wpt/tests/scroll-animations/css/animation-shorthand.html @@ -79,36 +79,6 @@ test((t) => { }); target.style.animation = 'anim 1s'; - target.style.animationDelayEnd = '42s'; - assert_equals(target.style.animation, ''); - assert_equals(target.style.animationName, 'anim'); - assert_equals(target.style.animationDuration, '1s'); - - target.style.animationDelayEnd = '0s, 0s'; - assert_equals(target.style.animation, ''); -}, 'Animation shorthand can not represent non-initial animation-delay-end (specified)'); - -test((t) => { - t.add_cleanup(() => { - target.style = ''; - }); - - target.style.animation = 'anim 1s'; - target.style.animationDelayEnd = '42s'; - assert_equals(getComputedStyle(target).animation, ''); - assert_equals(getComputedStyle(target).animationName, 'anim'); - assert_equals(getComputedStyle(target).animationDuration, '1s'); - - target.style.animationDelayEnd = '0s, 0s'; - assert_equals(getComputedStyle(target).animation, ''); -}, 'Animation shorthand can not represent non-initial animation-delay-end (computed)'); - -test((t) => { - t.add_cleanup(() => { - target.style = ''; - }); - - target.style.animation = 'anim 1s'; target.style.animationRangeStart = 'entry'; assert_equals(target.style.animation, ''); assert_equals(target.style.animationName, 'anim'); diff --git a/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-on-shadow-host.html b/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-on-shadow-host.html new file mode 100644 index 00000000000..a48eda1b57c --- /dev/null +++ b/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-on-shadow-host.html @@ -0,0 +1,43 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>CSS Display: reading-order-items with value grid-order on shadow host</title> +<link rel="help" href="https://drafts.csswg.org/css-display-4/#reading-order-items"> +<link rel="author" title="Di Zhang" href="mailto:dizhangg@chromium.org"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src='../../resources/shadow-dom.js'></script> +<script src="../../resources/focus-utils.js"></script> + +<style> +.wrapper { + display: grid; + reading-order-items: grid-order; +} +</style> + +<div class="test-case" data-expect="root/B,root/A" + data-description="Grid items in shadow host with delegatesFocus"> + <div id="root" class="wrapper" tabindex="0"> + <template shadowrootmode="open" shadowrootdelegatesfocus> + <button id="A" style="order: 2">A</button> + <button id="B" style="order: 1">B</button> + </template> + </div> +</div> + +<div class="test-case" data-expect="root2,root2/B,root2/A" + data-description="Grid items in shadow host without delegatesFocus"> + <div id="root2" class="wrapper" tabindex="0"> + <template shadowrootmode="open"> + <button id="A" style="order: 2">A</button> + <button id="B" style="order: 1">B</button> + </template> + </div> +</div> + +<script> +runFocusTestCases(); +</script> diff --git a/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-iframe.html b/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-iframe.html new file mode 100644 index 00000000000..87b9e0a83df --- /dev/null +++ b/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-iframe.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>CSS Display: reading-order-items with value grid-order on iframe</title> +<link rel="help" href="https://drafts.csswg.org/css-display-4/#reading-order-items"> +<link rel="author" title="Di Zhang" href="mailto:dizhangg@chromium.org"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src='../../resources/shadow-dom.js'></script> +<script src="../../resources/focus-utils.js"></script> + +<style> +.wrapper { + display: grid; + reading-order-items: grid-order; +} +</style> + +<div class="test-case" data-expect="start,frame2/B,frame1/A,end" + data-description="Grid items are iframes."> + <button id="start">Item Start</button> + <div class="wrapper"> + <iframe id="frame1" style="order: 2" srcdoc="<button id=A>A</button>"></iframe> + <iframe id="frame2" style="order: 1" srcdoc="<button id=B>B</button>"></iframe> + </div> + <button id="end">Item End</button> +</div> + +<script> +window.onload = runFocusTestCases; +</script> diff --git a/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-nested-grids.html b/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-nested-grids.html new file mode 100644 index 00000000000..d3da6682a34 --- /dev/null +++ b/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-nested-grids.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>CSS Display: reading-order-items with value grid-order in nested grids</title> +<link rel="help" href="https://drafts.csswg.org/css-display-4/#reading-order-items"> +<link rel="author" title="Di Zhang" href="mailto:dizhangg@chromium.org"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src='../../resources/shadow-dom.js'></script> +<script src="../../resources/focus-utils.js"></script> + +<style> +.box { + display: grid; + reading-order-items: grid-order; + border-radius: 5px; + padding: 20px; + border-style: dashed; +} + +</style> + +<div class="test-case" data-expect="w,d3,b3a,b3b,d2,b2,d1,b1a,b1b" + data-description="Grid items are not grid containers."> + <div class="box" id="w" tabindex="0"> + <div style="order: 2" id="d1" tabindex="0">Div 1 + <button id="b1a" style="order: 3">Button 1A</button> + <button id="b1b">Button 1B</button> + </div> + <div id="d2" tabindex="0">Div 2 + <button id="b2">Button 2</button> + </div> + <div id="d3" style="order: -1" tabindex="0">Div 3 + <button id="b3a" style="order: 2">Button 3A</button> + <button id="b3b">Button 3B</button> + </div> + </div> +</div> + +<div class="test-case" + data-expect="wrapper,div3,button3b,button3a,div2,button2,div1,button1b,button1a" + data-description="Grid items are grid containers."> + <div class="box" id="wrapper" tabindex="0"> + <div class="box" style="order: 2" id="div1" tabindex="0">Div 1 + <button id="button1a" style="order: 3">Button 1A</button> + <button id="button1b">Button 1B</button> + </div> + <div class="box" id="div2" tabindex="0">Div 2 + <button id="button2">Button 2</button> + </div> + <div class="box" id="div3" style="order: -1" tabindex="0">Div 3 + <button id="button3a" style="order: 2">Button 3A</button> + <button id="button3b">Button 3B</button> + </div> + </div> +</div> + +<div class="test-case" data-expect="a,b,d,c" + data-description="Only has grid containers."> + <div class="box" id="a" tabindex="0">A + <div class="box" id="b" tabindex="0">B + <div class="box" id="c" tabindex="0" style="order: 2">C</div> + <div class="box" id="d" tabindex="0">D</div> + </div> + </div> +</div> + +<script> +runFocusTestCases(); +</script> diff --git a/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-popover.html b/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-popover.html new file mode 100644 index 00000000000..2e98c041570 --- /dev/null +++ b/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-popover.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>CSS Display: reading-order-items with value grid-order</title> +<link rel="help" href="https://drafts.csswg.org/css-display-4/#reading-order-items"> +<link rel="author" title="Di Zhang" href="mailto:dizhangg@chromium.org"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src='../../resources/shadow-dom.js'></script> +<script src="../../resources/focus-utils.js"></script> + +<style> +.wrapper { + display: grid; + reading-order-items: grid-order; +} +.popover { + inset:auto; + top:200px; + left:200px; +} +</style> + +<div class="wrapper"> + <button id=a style="order: 4">A</button> + <button id=invoker style="order: 2" popovertarget=P>Invoker</button> + <button id=c style="order: 1">C</button> +</div> +<div popover id=P class="popover"> + <button id=b1 style="order: 3">Popover button B1</button> + <button id=b2 style="order: 1">Popover button B2</button> + <button id=b3 style="order: 2">Popover button B3</button> +</div> + +<script> +document.querySelector('[popovertarget]').click(); + +promise_test(async () => { + let elements = [ + 'c', + 'invoker', + 'b1', + 'b2', + 'b3', + 'a', + ]; + await assert_focus_navigation_forward(elements); +}, `Popover in reading-order-items: grid-order container.`); + +promise_test(async () => { + P.classList.add("wrapper"); + let elements = [ + 'c', + 'invoker', + 'b2', + 'b3', + 'b1', + 'a', + ]; + await assert_focus_navigation_forward(elements); +}, `Popover in container and itself with reading-order-items: grid-order.`); +</script>
\ No newline at end of file diff --git a/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-slots.html b/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-slots.html new file mode 100644 index 00000000000..3045001e1d8 --- /dev/null +++ b/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order-with-slots.html @@ -0,0 +1,164 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>CSS Display: reading-order-items with value grid-order in Shadow DOM</title> +<link rel="help" href="https://drafts.csswg.org/css-display-4/#reading-order-items"> +<link rel="author" title="Di Zhang" href="mailto:dizhangg@chromium.org"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src='../../resources/shadow-dom.js'></script> +<script src="../../resources/focus-utils.js"></script> + +<style> +.wrapper { + display: grid; + reading-order-items: grid-order; +} +</style> + +<span id="root1" class="test-case" data-expect="b1,a1,c1" + data-description="Slot assigned element is a grid with reading-order-items."> + <template shadowrootmode="open"> + <style> + .wrapper { + display: grid; + reading-order-items: grid-order; + } + </style> + <slot></slot> + </template> + <div class="wrapper"> + <button style="order: 2" id="a1">Item A</button> + <button style="order: 1" id="b1">Item B</button> + <button style="order: 3" id="c1">Item C</button> + </div> +</span> +<br> + +<span id="root2" class="test-case" data-expect="b2,a2,c2" + data-description="Slot is a grid with reading-order-items."> + <template shadowrootmode="open"> + <style> + .wrapper { + display: grid; + reading-order-items: grid-order; + } + </style> + <slot class="wrapper"></slot> + </template> + <button style="order: 2" id="a2">Item A</button> + <button style="order: 1" id="b2">Item B</button> + <button style="order: 3" id="c2">Item C</button> +</span> +<br> + +<span id="root3" class="test-case" data-expect="root3/o2,root3/o4,o1,o3,o5" + data-description="Slot is inside a grid container with reading-order-items."> + <template shadowrootmode="open"> + <style> + .wrapper { + display: grid; + reading-order-items: grid-order; + } + </style> + <div class="wrapper"> + <button style="order: 4" id="o4">Order 4</button> + <slot style="order: 10"></slot> + <button style="order: 2" id="o2">Order 2</button> + </div> + </template> + <button style="order: 5" id="o5">Order 5</button> + <button style="order: 1" id="o1">Order 1</button> + <button style="order: 3" id="o3">Order 3</button> +</span> +<br> + +<span id="root4" class="test-case" + data-expect="root4/after,root4/before,b4,a4,d42,d41,d43,c4" + data-description="Slot is a grid with reading-order-items inside a grid container with reading-order-items."> + <template shadowrootmode="open"> + <style> + .wrapper { + display: grid; + reading-order-items: grid-order; + } + </style> + <div class="wrapper"> + <button style="order: 4" id="before">Before</button> + <slot style="order: 10"></slot> + <button style="order: 2" id="after">After</button> + </div> + </template> + <button style="order: 3" id="a4">Item A</button> + <button style="order: 1" id="b4">Item B</button> + <button style="order: 6" id="c4">Item C</button> + <div style="order: 5" class="wrapper"> + <button style="order: 2" id="d41">Item D1</button> + <button style="order: 1" id="d42">Item D2</button> + <button style="order: 3" id="d43">Item D3</button> + </div> +</span> +<br> + +<span id="root5" class="test-case" data-expect="b51,a51,c51,b52,a52,c52" + data-description="Slot is not inside a shadow root."> + <div class="wrapper"> + <slot name=slot1> + <button style="order: 2" id="a51">Item A</button> + <button style="order: 1" id="b51">Item B</button> + <button style="order: 3" id="c51">Item C</button> + </slot> + </div> + <slot class="wrapper" name=slot2> + <button style="order: 2" id="a52">Item A</button> + <button style="order: 1" id="b52">Item B</button> + <button style="order: 3" id="c52">Item C</button> + </slot> +</span> +<br> + +<span id="root6" class="test-case" data-expect="root6/after,root6/before,b6,a6" + data-description="Slot is a display contents inside a grid container."> + <template shadowrootmode="open"> + <style> + .wrapper { + display: grid; + reading-order-items: grid-order; + } + </style> + <div class="wrapper"> + <button style="order: 4" id="before">Before</button> + <slot style="display: contents" style="order: 4"></slot> + <button style="order: 3" id="after">After</button> + </div> + </template> + <button id="a6" style="order: 7">A</button> + <button id="b6" style="order: 2">B</button> +</span> +<br> + +<span id="root7" class="test-case" data-expect="a7,b7,root7/after,root7/before" + data-description="Slot is a display block inside a grid container."> + <template shadowrootmode="open"> + <style> + .wrapper { + display: grid; + reading-order-items: grid-order; + } + </style> + <div class="wrapper"> + <button style="order: 4" id="before">Before</button> + <slot style="display: block" style="order: 4"></slot> + <button style="order: 3" id="after">After</button> + </div> + </template> + <button id="a7" style="order: 7">A</button> + <button id="b7" style="order: 2">B</button> +</span> +<br> + +<script> +runFocusTestCases(); +</script> diff --git a/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order.html b/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order.html new file mode 100644 index 00000000000..1b86ab0b25a --- /dev/null +++ b/tests/wpt/tests/shadow-dom/focus-navigation/reading-order/tentative/grid-order.html @@ -0,0 +1,89 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>CSS Display: reading-order-items with value grid-order</title> +<link rel="help" href="https://drafts.csswg.org/css-display-4/#reading-order-items"> +<link rel="author" title="Di Zhang" href="mailto:dizhangg@chromium.org"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/resources/testdriver-actions.js"></script> +<script src='../../resources/shadow-dom.js'></script> +<script src="../../resources/focus-utils.js"></script> + +<style> +.wrapper { + display: grid; + reading-order-items: grid-order; +} +</style> + +<div class="test-case" data-expect="a1,b1,c1,d1,e1" + data-description="Grid items without `order` property"> + <div class="wrapper"> + <button id="a1">Item A</button> + <button id="b1">Item B</button> + <button id="c1">Item C</button> + </div> + <div class="wrapper"> + <button id="d1">Item D</button> + <button id="e1">Item E</button> + </div> +</div> + +<div class="test-case" data-expect="b2,c2,a2,e2,d2" + data-description="Grid items with `order` property"> + <div class="wrapper"> + <button id="a2" style="order: 2">Item A</button> + <button id="b2">Item B</button> + <button id="c2">Item C</button> + </div> + <div class="wrapper"> + <button id="d2">Item D</button> + <button id="e2" style="order: -1">Item E</button> + </div> +</div> + +<div class="test-case" data-expect="c3,a3,b3,d3,f3,e3" + data-description="Grid items with `order` property and tabindex"> + <div class="wrapper"> + <button id="a3" style="order: -1">Item A</button> + <button id="b3" style="order: 0">Item B</button> + <button id="c3" tabindex="1" style="order: -1">Item C</button> + </div> + <div class="wrapper"> + <button id="d3" tabindex="1" style="order: 1">Item D</button> + <button id="e3" tabindex="2" style="order: 0">Item E</button> + <button id="f3" tabindex="2" style="order: -1">Item F</button> + </div> +</div> + +<div class="test-case" data-expect="order1,order2,order3,order4" + data-description="Items in display contents are sorted in same grid container."> + <div class="wrapper"> + <div style="display: contents"> + <button id="order3" style="order: 3">Order 3</button> + <button id="order1" style="order: 1">Order 1</button> + <div style="display: contents"> + <button id="order4" style="order: 4">Order 4</button> + <button id="order2" style="order: 2">Order 2</button> + </div> + </div> + </div> +</div> + +<div class="test-case" data-expect="d4,b4,c4,a4" + data-description="Items in display block are not in parent grid container."> + <div class="wrapper"> + <button id="a4" style="order: 3">A</button> + <div style="order: 1" style="display: block"> + <button id="b4" style="order: 2">B</button> + <button id="c4" style="order: -1">C</button> + </div> + <button id="d4">D</button> + </div> +</div> + +<script> +runFocusTestCases(); +</script> diff --git a/tests/wpt/tests/shadow-dom/focus-navigation/resources/focus-utils.js b/tests/wpt/tests/shadow-dom/focus-navigation/resources/focus-utils.js index 0392cfb8879..f4056dc1688 100644 --- a/tests/wpt/tests/shadow-dom/focus-navigation/resources/focus-utils.js +++ b/tests/wpt/tests/shadow-dom/focus-navigation/resources/focus-utils.js @@ -172,3 +172,18 @@ async function assert_focus_navigation_bidirectional_with_shadow_root(elements) await assert_focus_navigation_backward_with_shadow_root(elements); } +// This Promise will run each test case that is: +// 1. Wrapped in an element with class name "test-case". +// 2. Has data-expect attribute be an ordered list of elements to focus. +// 3. Has data-description attribute be a string explaining the test. +// e.g <div class="test-case" data-expect="b,a,c" +// data-description="Focus navigation"> +async function runFocusTestCases() { + const testCases = Array.from(document.querySelectorAll('.test-case')); + for (let testCase of testCases) { + promise_test(async () => { + const expected = testCase.dataset.expect.split(','); + await assert_focus_navigation_forward(expected); + }, testCase.dataset.description); + } +} diff --git a/tests/wpt/tests/shared-storage/cross-origin-create-worklet-failure-false-shared-storage-cross-origin-worklet-allowed.tentative.https.sub.html b/tests/wpt/tests/shared-storage/cross-origin-create-worklet-unrevealed-failure-false-shared-storage-cross-origin-worklet-allowed.tentative.https.sub.html index db361776f63..f1f37b0affd 100644 --- a/tests/wpt/tests/shared-storage/cross-origin-create-worklet-failure-false-shared-storage-cross-origin-worklet-allowed.tentative.https.sub.html +++ b/tests/wpt/tests/shared-storage/cross-origin-create-worklet-unrevealed-failure-false-shared-storage-cross-origin-worklet-allowed.tentative.https.sub.html @@ -19,10 +19,11 @@ promise_test(async t => { `&shared_storage_cross_origin_worklet_allowed_header=?0` + `&token=${ancestor_key}`; - return promise_rejects_dom(t, "OperationError", - sharedStorage.createWorklet( + // The network error for `createWorklet()` won't be revealed to the + // cross-origin caller. + await sharedStorage.createWorklet( helper_url + `&action=store-cookie`, - { credentials: "include" })); + { credentials: "include" }); }, 'createWorklet() with cross-origin module script and credentials ' + '"include", and with the Shared-Storage-Cross-Origin-Worklet-Allowed ' + 'response header value set to false (?0)'); diff --git a/tests/wpt/tests/shared-storage/cross-origin-create-worklet-failure-missing-access-control-allow-credentials.tentative.https.sub.html b/tests/wpt/tests/shared-storage/cross-origin-create-worklet-unrevealed-failure-missing-access-control-allow-credentials.tentative.https.sub.html index 8887aad64dc..dd6347e4631 100644 --- a/tests/wpt/tests/shared-storage/cross-origin-create-worklet-failure-missing-access-control-allow-credentials.tentative.https.sub.html +++ b/tests/wpt/tests/shared-storage/cross-origin-create-worklet-unrevealed-failure-missing-access-control-allow-credentials.tentative.https.sub.html @@ -18,10 +18,11 @@ promise_test(async t => { `&shared_storage_cross_origin_worklet_allowed_header=?1` + `&token=${ancestor_key}`; - return promise_rejects_dom(t, "OperationError", - sharedStorage.createWorklet( + // The network error for `createWorklet()` won't be revealed to the + // cross-origin caller. + await sharedStorage.createWorklet( helper_url + `&action=store-cookie`, - { credentials: "include" })); + { credentials: "include" }); }, 'createWorklet() with cross-origin module script and credentials ' + '"include", and without the Access-Control-Allow-Credentials response ' + 'header'); diff --git a/tests/wpt/tests/shared-storage/cross-origin-create-worklet-failure-missing-access-control-allow-origin.tentative.https.sub.html b/tests/wpt/tests/shared-storage/cross-origin-create-worklet-unrevealed-failure-missing-access-control-allow-origin.tentative.https.sub.html index 58a2f3a77bb..1f3223a5644 100644 --- a/tests/wpt/tests/shared-storage/cross-origin-create-worklet-failure-missing-access-control-allow-origin.tentative.https.sub.html +++ b/tests/wpt/tests/shared-storage/cross-origin-create-worklet-unrevealed-failure-missing-access-control-allow-origin.tentative.https.sub.html @@ -18,10 +18,11 @@ promise_test(async t => { `&shared_storage_cross_origin_worklet_allowed_header=?1` + `&token=${ancestor_key}`; - return promise_rejects_dom(t, "OperationError", - sharedStorage.createWorklet( + // The network error for `createWorklet()` won't be revealed to the + // cross-origin caller. + await sharedStorage.createWorklet( helper_url + `&action=store-cookie`, - { credentials: "include" })); + { credentials: "include" }); }, 'createWorklet() with cross-origin module script and credentials ' + '"include", and without the Access-Control-Allow-Origin response header'); diff --git a/tests/wpt/tests/shared-storage/cross-origin-create-worklet-failure-missing-shared-storage-cross-origin-worklet-allowed.tentative.https.sub.html b/tests/wpt/tests/shared-storage/cross-origin-create-worklet-unrevealed-failure-missing-shared-storage-cross-origin-worklet-allowed.tentative.https.sub.html index 5b140a8141c..f96e4d596e8 100644 --- a/tests/wpt/tests/shared-storage/cross-origin-create-worklet-failure-missing-shared-storage-cross-origin-worklet-allowed.tentative.https.sub.html +++ b/tests/wpt/tests/shared-storage/cross-origin-create-worklet-unrevealed-failure-missing-shared-storage-cross-origin-worklet-allowed.tentative.https.sub.html @@ -18,10 +18,11 @@ promise_test(async t => { `&access_control_allow_credentials_header=true` + `&token=${ancestor_key}`; - return promise_rejects_dom(t, "OperationError", - sharedStorage.createWorklet( + // The network error for `createWorklet()`` won't be revealed to the + // cross-origin caller. + await sharedStorage.createWorklet( helper_url + `&action=store-cookie`, - { credentials: "include" })); + { credentials: "include" }); }, 'createWorklet() with cross-origin module script and credentials ' + '"include", and without the Shared-Storage-Cross-Origin-Worklet-Allowed ' + 'response header'); diff --git a/tests/wpt/tests/shared-storage/cross-origin-worklet-select-url-and-verify-data-origin.tentative.https.sub.html b/tests/wpt/tests/shared-storage/cross-origin-worklet-select-url-and-verify-data-origin.tentative.https.sub.html new file mode 100644 index 00000000000..5b6b9d5f8f5 --- /dev/null +++ b/tests/wpt/tests/shared-storage/cross-origin-worklet-select-url-and-verify-data-origin.tentative.https.sub.html @@ -0,0 +1,46 @@ +<!doctype html> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/common/utils.js"></script> +<script src="/shared-storage/resources/util.js"></script> +<script src="/fenced-frame/resources/utils.js"></script> + +<body> +<script> +'use strict'; + +promise_test(async () => { + const crossOrigin = 'https://{{domains[www]}}:{{ports[https][0]}}'; + const script_url = crossOrigin + + `/shared-storage/resources/simple-module.js`; + + const worklet = await sharedStorage.createWorklet( + script_url, + { credentials: "omit" }); + + const ancestor_key = token(); + let url0 = generateURL("/shared-storage/resources/frame0.html", + [ancestor_key]); + + let select_url_result = await worklet.selectURL( + "test-url-selection-operation", + [{ url: url0 }], { + data: { + 'mockResult': 0, + 'setKey': 'key0', + 'setValue': 'value0' + }, + resolveToConfig: true, + keepAlive: true + }); + + assert_true(validateSelectURLResult(select_url_result, true)); + attachFencedFrame(select_url_result, 'opaque-ads'); + const result0 = await nextValueFromServer(ancestor_key); + assert_equals(result0, "frame0_loaded"); + + await verifyKeyValueForOrigin('key0', 'value0', crossOrigin); +}, 'For a cross-origin worklet, test selectURL() and verify its data origin'); + +</script> +</body> diff --git a/tests/wpt/tests/shared-storage/resources/simple-module.js b/tests/wpt/tests/shared-storage/resources/simple-module.js index 620a3592f27..11b650811dc 100644 --- a/tests/wpt/tests/shared-storage/resources/simple-module.js +++ b/tests/wpt/tests/shared-storage/resources/simple-module.js @@ -6,6 +6,10 @@ var globalVar = 0; class TestURLSelectionOperation { async run(urls, data) { + if (data && data.hasOwnProperty('setKey') && data.hasOwnProperty('setValue')) { + await sharedStorage.set(data['setKey'], data['setValue']); + } + if (data && data.hasOwnProperty('mockResult')) { return data['mockResult']; } diff --git a/tests/wpt/tests/shared-storage/resources/simple-module.js.headers b/tests/wpt/tests/shared-storage/resources/simple-module.js.headers new file mode 100644 index 00000000000..cf3e03e24c7 --- /dev/null +++ b/tests/wpt/tests/shared-storage/resources/simple-module.js.headers @@ -0,0 +1,2 @@ +Access-Control-Allow-Origin: * +Shared-Storage-Cross-Origin-Worklet-Allowed: ?1 diff --git a/tests/wpt/tests/streams/piping/detached-context-crash.html b/tests/wpt/tests/streams/piping/detached-context-crash.html deleted file mode 100644 index 56271f12c3a..00000000000 --- a/tests/wpt/tests/streams/piping/detached-context-crash.html +++ /dev/null @@ -1,21 +0,0 @@ -<!DOCTYPE html> -<body> -<script> -window.onload = () => { - const i = document.createElement("iframe"); - i.src = "about:blank"; - document.body.appendChild(i); - - const rs = new i.contentWindow.ReadableStream({ - start(controller) { controller.error(); } - }); - const ws = new i.contentWindow.WritableStream(); - - i.remove(); - - // pipeTo() should not crash with a ReadableStream or WritableStream from - // a detached iframe. - rs.pipeTo(ws); -}; -</script> -</body> diff --git a/tests/wpt/tests/streams/readable-streams/async-iterator.any.js b/tests/wpt/tests/streams/readable-streams/async-iterator.any.js index 4b674bea843..e192201b531 100644 --- a/tests/wpt/tests/streams/readable-streams/async-iterator.any.js +++ b/tests/wpt/tests/streams/readable-streams/async-iterator.any.js @@ -475,16 +475,86 @@ promise_test(async () => { const rs = new ReadableStream(); const it = rs.values(); - const iterResults = await Promise.allSettled([it.return('return value'), it.next()]); + const resolveOrder = []; + const iterResults = await Promise.allSettled([ + it.return('return value').then(result => { + resolveOrder.push('return'); + return result; + }), + it.next().then(result => { + resolveOrder.push('next'); + return result; + }) + ]); assert_equals(iterResults[0].status, 'fulfilled', 'return() promise status'); assert_iter_result(iterResults[0].value, 'return value', true, 'return()'); assert_equals(iterResults[1].status, 'fulfilled', 'next() promise status'); assert_iter_result(iterResults[1].value, undefined, true, 'next()'); + + assert_array_equals(resolveOrder, ['return', 'next'], 'next() resolves after return()'); }, 'return(); next() [no awaiting]'); promise_test(async () => { + let resolveCancelPromise; + const rs = recordingReadableStream({ + cancel(reason) { + return new Promise(r => resolveCancelPromise = r); + } + }); + const it = rs.values(); + + let returnResolved = false; + const returnPromise = it.return('return value').then(result => { + returnResolved = true; + return result; + }); + await flushAsyncEvents(); + assert_false(returnResolved, 'return() should not resolve while cancel() promise is pending'); + + resolveCancelPromise(); + const iterResult1 = await returnPromise; + assert_iter_result(iterResult1, 'return value', true, 'return()'); + + const iterResult2 = await it.next(); + assert_iter_result(iterResult2, undefined, true, 'next()'); +}, 'return(); next() with delayed cancel()'); + +promise_test(async () => { + let resolveCancelPromise; + const rs = recordingReadableStream({ + cancel(reason) { + return new Promise(r => resolveCancelPromise = r); + } + }); + const it = rs.values(); + + const resolveOrder = []; + const returnPromise = it.return('return value').then(result => { + resolveOrder.push('return'); + return result; + }); + const nextPromise = it.next().then(result => { + resolveOrder.push('next'); + return result; + }); + + assert_array_equals(rs.events, ['cancel', 'return value'], 'return() should call cancel()'); + assert_array_equals(resolveOrder, [], 'return() should not resolve before cancel() resolves'); + + resolveCancelPromise(); + const iterResult1 = await returnPromise; + assert_iter_result(iterResult1, 'return value', true, 'return() should resolve with original reason'); + const iterResult2 = await nextPromise; + assert_iter_result(iterResult2, undefined, true, 'next() should resolve with done result'); + + assert_array_equals(rs.events, ['cancel', 'return value'], 'no pull() after cancel()'); + assert_array_equals(resolveOrder, ['return', 'next'], 'next() should resolve after return() resolves'); + +}, 'return(); next() with delayed cancel() [no awaiting]'); + +promise_test(async () => { const rs = new ReadableStream(); const it = rs.values(); @@ -499,13 +569,25 @@ promise_test(async () => { const rs = new ReadableStream(); const it = rs.values(); - const iterResults = await Promise.allSettled([it.return('return value 1'), it.return('return value 2')]); + const resolveOrder = []; + const iterResults = await Promise.allSettled([ + it.return('return value 1').then(result => { + resolveOrder.push('return 1'); + return result; + }), + it.return('return value 2').then(result => { + resolveOrder.push('return 2'); + return result; + }) + ]); assert_equals(iterResults[0].status, 'fulfilled', '1st return() promise status'); assert_iter_result(iterResults[0].value, 'return value 1', true, '1st return()'); assert_equals(iterResults[1].status, 'fulfilled', '2nd return() promise status'); assert_iter_result(iterResults[1].value, 'return value 2', true, '1st return()'); + + assert_array_equals(resolveOrder, ['return 1', 'return 2'], '2nd return() resolves after 1st return()'); }, 'return(); return() [no awaiting]'); test(() => { diff --git a/tests/wpt/tests/tools/ci/azure/install_chrome.yml b/tests/wpt/tests/tools/ci/azure/install_chrome.yml index 7599321be24..9b03d9b91bc 100644 --- a/tests/wpt/tests/tools/ci/azure/install_chrome.yml +++ b/tests/wpt/tests/tools/ci/azure/install_chrome.yml @@ -5,7 +5,7 @@ steps: set -eux -o pipefail HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --cask google-chrome || true HOMEBREW_NO_AUTO_UPDATE=1 brew uninstall --cask chromedriver || true - curl https://raw.githubusercontent.com/Homebrew/homebrew-cask-versions/master/Casks/google-chrome-dev.rb > google-chrome-dev.rb - HOMEBREW_NO_AUTO_UPDATE=1 brew install --cask google-chrome-dev.rb + curl https://raw.githubusercontent.com/Homebrew/homebrew-cask/HEAD/Casks/g/google-chrome@dev.rb > google-chrome@dev.rb + HOMEBREW_NO_AUTO_UPDATE=1 brew install --cask google-chrome@dev.rb displayName: 'Install Chrome Dev' condition: and(succeeded(), eq(variables['Agent.OS'], 'Darwin')) diff --git a/tests/wpt/tests/tools/ci/requirements_build.txt b/tests/wpt/tests/tools/ci/requirements_build.txt index 54f21efbd98..7b4f8619b2e 100644 --- a/tests/wpt/tests/tools/ci/requirements_build.txt +++ b/tests/wpt/tests/tools/ci/requirements_build.txt @@ -1,5 +1,5 @@ -cairocffi==1.6.1 -fonttools==4.47.2 +cairocffi==1.7.0 +fonttools==4.51.0 genshi==0.7.7 jinja2==3.1.3 pyyaml==6.0.1 diff --git a/tests/wpt/tests/tools/ci/requirements_macos_color_profile.txt b/tests/wpt/tests/tools/ci/requirements_macos_color_profile.txt index 7505a98d9f3..8e178d1d2c4 100644 --- a/tests/wpt/tests/tools/ci/requirements_macos_color_profile.txt +++ b/tests/wpt/tests/tools/ci/requirements_macos_color_profile.txt @@ -1,4 +1,4 @@ -pyobjc-core==9.2 +pyobjc-core==10.2 pyobjc-framework-Cocoa==9.2 pyobjc-framework-ColorSync==9.2 pyobjc-framework-Quartz==9.2 diff --git a/tests/wpt/tests/tools/ci/requirements_tc.txt b/tests/wpt/tests/tools/ci/requirements_tc.txt index e1ae4dbf70e..aa57643b9b5 100644 --- a/tests/wpt/tests/tools/ci/requirements_tc.txt +++ b/tests/wpt/tests/tools/ci/requirements_tc.txt @@ -1,4 +1,4 @@ -pygithub==2.2.0 +pygithub==2.3.0 pyyaml==6.0.1 requests==2.31.0 -taskcluster==60.4.1 +taskcluster==64.2.7 diff --git a/tests/wpt/tests/tools/manifest/item.py b/tests/wpt/tests/tools/manifest/item.py index 86f7bd6020b..e25f7ca2c29 100644 --- a/tests/wpt/tests/tools/manifest/item.py +++ b/tests/wpt/tests/tools/manifest/item.py @@ -279,7 +279,7 @@ class PrintRefTest(RefTest): @property def page_ranges(self) -> PageRanges: - return self._extras.get("page_ranges", {}) + return cast(PageRanges, self._extras.get("page_ranges", {})) def to_json(self): # type: ignore rv = super().to_json() diff --git a/tests/wpt/tests/tools/manifest/sourcefile.py b/tests/wpt/tests/tools/manifest/sourcefile.py index 6da919b3b66..3563fb9e5e9 100644 --- a/tests/wpt/tests/tools/manifest/sourcefile.py +++ b/tests/wpt/tests/tools/manifest/sourcefile.py @@ -4,8 +4,8 @@ import os from collections import deque from fnmatch import fnmatch from io import BytesIO -from typing import (Any, BinaryIO, Callable, Deque, Dict, Iterable, List, Optional, Pattern, - Set, Text, Tuple, Union, cast) +from typing import (Any, BinaryIO, Callable, Deque, Dict, Iterable, List, + Optional, Pattern, Set, Text, Tuple, TypedDict, Union, cast) from urllib.parse import urljoin try: @@ -68,7 +68,13 @@ def read_script_metadata(f: BinaryIO, regexp: Pattern[bytes]) -> Iterable[Tuple[ yield (m.groups()[0].decode("utf8"), m.groups()[1].decode("utf8")) -_any_variants: Dict[Text, Dict[Text, Any]] = { +class VariantData(TypedDict, total=False): + suffix: str + force_https: bool + longhand: Set[str] + + +_any_variants: Dict[Text, VariantData] = { "window": {"suffix": ".any.html"}, "window-module": {}, "serviceworker": {"force_https": True}, diff --git a/tests/wpt/tests/tools/requirements_flake8.txt b/tests/wpt/tests/tools/requirements_flake8.txt index fc1f92a69f7..3f7f3121ca2 100644 --- a/tests/wpt/tests/tools/requirements_flake8.txt +++ b/tests/wpt/tests/tools/requirements_flake8.txt @@ -1,7 +1,5 @@ flake8==5.0.4; python_version < '3.9' -pycodestyle==2.9.1; python_version < '3.8' -pyflakes==2.5.0; python_version < '3.8' flake8==6.1.0; python_version >= '3.9' -pycodestyle==2.11.0; python_version >= '3.9' +pycodestyle==2.11.1; python_version >= '3.9' pyflakes==3.1.0; python_version >= '3.9' pep8-naming==0.13.3 diff --git a/tests/wpt/tests/tools/requirements_mypy.txt b/tests/wpt/tests/tools/requirements_mypy.txt index c3db2292af5..3b1d3b03d64 100644 --- a/tests/wpt/tests/tools/requirements_mypy.txt +++ b/tests/wpt/tests/tools/requirements_mypy.txt @@ -1,14 +1,14 @@ -mypy==1.4.1 +mypy==1.10.0 mypy-extensions==1.0.0 toml==0.10.2 tomli==2.0.1 typed-ast==1.5.5 types-atomicwrites==1.4.5.1 -types-python-dateutil==2.8.19.14 +types-python-dateutil==2.9.0.20240316 types-PyYAML==6.0.12.12 types-requests==2.31.0.20231231 -types-setuptools==68.0.0.3 -types-six==1.16.21.9 +types-setuptools==69.5.0.20240423 +types-six==1.16.21.20240425 types-ujson==5.9.0.0 types-urllib3==1.26.25.14 typing_extensions==4.7.1 diff --git a/tests/wpt/tests/tools/requirements_pytest.txt b/tests/wpt/tests/tools/requirements_pytest.txt index 9034cda7190..64d38583a2f 100644 --- a/tests/wpt/tests/tools/requirements_pytest.txt +++ b/tests/wpt/tests/tools/requirements_pytest.txt @@ -1,3 +1,3 @@ pytest==7.4.4 pytest-cov==4.1.0 -hypothesis==6.78.2 +hypothesis==6.100.2 diff --git a/tests/wpt/tests/tools/requirements_tests.txt b/tests/wpt/tests/tools/requirements_tests.txt index 6455286736d..2613def3da4 100644 --- a/tests/wpt/tests/tools/requirements_tests.txt +++ b/tests/wpt/tests/tools/requirements_tests.txt @@ -1,6 +1,6 @@ -httpx[http2]==0.24.1 +httpx[http2]==0.27.0 json-e==4.5.3 jsonschema==4.17.3 pyyaml==6.0.1 -taskcluster==60.4.1 +taskcluster==64.2.7 mozterm==1.0.0 diff --git a/tests/wpt/tests/tools/third_party/websockets/.github/FUNDING.yml b/tests/wpt/tests/tools/third_party/websockets/.github/FUNDING.yml new file mode 100644 index 00000000000..c6c5426a5a6 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/.github/FUNDING.yml @@ -0,0 +1,3 @@ +github: python-websockets +open_collective: websockets +tidelift: pypi/websockets diff --git a/tests/wpt/tests/tools/third_party/websockets/.github/ISSUE_TEMPLATE/config.yml b/tests/wpt/tests/tools/third_party/websockets/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..3ba13e0cec6 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false diff --git a/tests/wpt/tests/tools/third_party/websockets/.github/ISSUE_TEMPLATE/issue.md b/tests/wpt/tests/tools/third_party/websockets/.github/ISSUE_TEMPLATE/issue.md new file mode 100644 index 00000000000..3cf4e3b7701 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/.github/ISSUE_TEMPLATE/issue.md @@ -0,0 +1,29 @@ +--- +name: Report an issue +about: Let us know about a problem with websockets +title: '' +labels: '' +assignees: '' + +--- + +<!-- + +Thanks for taking the time to report an issue! + +Did you check the FAQ? Perhaps you'll find the answer you need: +https://websockets.readthedocs.io/en/stable/faq/index.html + +Is your question really about asyncio? Perhaps the dev guide will help: +https://docs.python.org/3/library/asyncio-dev.html + +Did you look for similar issues? Please keep the discussion in one place :-) +https://github.com/python-websockets/websockets/issues?q=is%3Aissue + +Is your issue related to cryptocurrency in any way? Please don't file it. +https://websockets.readthedocs.io/en/stable/project/contributing.html#cryptocurrency-users + +For bugs, providing a reproduction helps a lot. Take an existing example and tweak it! +https://github.com/python-websockets/websockets/tree/main/example + +--> diff --git a/tests/wpt/tests/tools/third_party/websockets/.github/dependabot.yml b/tests/wpt/tests/tools/third_party/websockets/.github/dependabot.yml new file mode 100644 index 00000000000..ad1e824b4a5 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "saturday" + time: "07:00" + timezone: "Europe/Paris" diff --git a/tests/wpt/tests/tools/third_party/websockets/.github/workflows/tests.yml b/tests/wpt/tests/tools/third_party/websockets/.github/workflows/tests.yml new file mode 100644 index 00000000000..470f5bc9606 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/.github/workflows/tests.yml @@ -0,0 +1,83 @@ +name: Run tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +env: + WEBSOCKETS_TESTS_TIMEOUT_FACTOR: 10 + +jobs: + coverage: + name: Run test coverage checks + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Install Python 3.x + uses: actions/setup-python@v4 + with: + python-version: "3.x" + - name: Install tox + run: pip install tox + - name: Run tests with coverage + run: tox -e coverage + - name: Run tests with per-module coverage + run: tox -e maxi_cov + + quality: + name: Run code quality checks + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Install Python 3.x + uses: actions/setup-python@v4 + with: + python-version: "3.x" + - name: Install tox + run: pip install tox + - name: Check code formatting + run: tox -e black + - name: Check code style + run: tox -e ruff + - name: Check types statically + run: tox -e mypy + + matrix: + name: Run tests on Python ${{ matrix.python }} + needs: + - coverage + - quality + runs-on: ubuntu-latest + strategy: + matrix: + python: + - "3.8" + - "3.9" + - "3.10" + - "3.11" + - "pypy-3.8" + - "pypy-3.9" + is_main: + - ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + exclude: + - python: "pypy-3.8" + is_main: false + - python: "pypy-3.9" + is_main: false + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Install Python ${{ matrix.python }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + - name: Install tox + run: pip install tox + - name: Run tests + run: tox -e py diff --git a/tests/wpt/tests/tools/third_party/websockets/.github/workflows/wheels.yml b/tests/wpt/tests/tools/third_party/websockets/.github/workflows/wheels.yml new file mode 100644 index 00000000000..707ef2c60d1 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/.github/workflows/wheels.yml @@ -0,0 +1,88 @@ +name: Build wheels + +on: + push: + tags: + - '*' + workflow_dispatch: + +jobs: + sdist: + name: Build source distribution and architecture-independent wheel + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Install Python 3.x + uses: actions/setup-python@v4 + with: + python-version: 3.x + - name: Build sdist + run: python setup.py sdist + - name: Save sdist + uses: actions/upload-artifact@v3 + with: + path: dist/*.tar.gz + - name: Install wheel + run: pip install wheel + - name: Build wheel + env: + BUILD_EXTENSION: no + run: python setup.py bdist_wheel + - name: Save wheel + uses: actions/upload-artifact@v3 + with: + path: dist/*.whl + + wheels: + name: Build architecture-specific wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: + - ubuntu-latest + - windows-latest + - macOS-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + - name: Install Python 3.x + uses: actions/setup-python@v4 + with: + python-version: 3.x + - name: Set up QEMU + if: runner.os == 'Linux' + uses: docker/setup-qemu-action@v3 + with: + platforms: all + - name: Build wheels + uses: pypa/cibuildwheel@v2.16.2 + env: + BUILD_EXTENSION: yes + - name: Save wheels + uses: actions/upload-artifact@v3 + with: + path: wheelhouse/*.whl + + release: + name: Release + needs: + - sdist + - wheels + runs-on: ubuntu-latest + # Don't release when running the workflow manually from GitHub's UI. + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') + steps: + - name: Download artifacts + uses: actions/download-artifact@v3 + with: + name: artifact + path: dist + - name: Upload to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + password: ${{ secrets.PYPI_API_TOKEN }} + - name: Create GitHub release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release create ${{ github.ref_name }} --notes "See https://websockets.readthedocs.io/en/stable/project/changelog.html for details." diff --git a/tests/wpt/tests/tools/third_party/websockets/.gitignore b/tests/wpt/tests/tools/third_party/websockets/.gitignore new file mode 100644 index 00000000000..324e77069a9 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/.gitignore @@ -0,0 +1,16 @@ +*.pyc +*.so +.coverage +.direnv +.envrc +.idea/ +.mypy_cache +.tox +build/ +compliance/reports/ +experiments/compression/corpus.pkl +dist/ +docs/_build/ +htmlcov/ +MANIFEST +websockets.egg-info/ diff --git a/tests/wpt/tests/tools/third_party/websockets/.readthedocs.yml b/tests/wpt/tests/tools/third_party/websockets/.readthedocs.yml new file mode 100644 index 00000000000..0369e065654 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/.readthedocs.yml @@ -0,0 +1,13 @@ +version: 2 + +build: + os: ubuntu-20.04 + tools: + python: "3.10" + +sphinx: + configuration: docs/conf.py + +python: + install: + - requirements: docs/requirements.txt diff --git a/tests/wpt/tests/tools/third_party/websockets/CODE_OF_CONDUCT.md b/tests/wpt/tests/tools/third_party/websockets/CODE_OF_CONDUCT.md new file mode 100644 index 00000000000..80f80d51b11 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at aymeric DOT augustin AT fractalideas DOT com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/tests/wpt/tests/tools/third_party/websockets/LICENSE b/tests/wpt/tests/tools/third_party/websockets/LICENSE index 119b29ef35b..5d61ece22a7 100644 --- a/tests/wpt/tests/tools/third_party/websockets/LICENSE +++ b/tests/wpt/tests/tools/third_party/websockets/LICENSE @@ -1,5 +1,4 @@ -Copyright (c) 2013-2021 Aymeric Augustin and contributors. -All rights reserved. +Copyright (c) Aymeric Augustin and contributors Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -9,9 +8,9 @@ modification, are permitted provided that the following conditions are met: * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - * Neither the name of websockets nor the names of its contributors may - be used to endorse or promote products derived from this software without - specific prior written permission. + * Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED diff --git a/tests/wpt/tests/tools/third_party/websockets/Makefile b/tests/wpt/tests/tools/third_party/websockets/Makefile new file mode 100644 index 00000000000..cf3b5339394 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/Makefile @@ -0,0 +1,35 @@ +.PHONY: default style types tests coverage maxi_cov build clean + +export PYTHONASYNCIODEBUG=1 +export PYTHONPATH=src +export PYTHONWARNINGS=default + +default: style types tests + +style: + black src tests + ruff --fix src tests + +types: + mypy --strict src + +tests: + python -m unittest + +coverage: + coverage run --source src/websockets,tests -m unittest + coverage html + coverage report --show-missing --fail-under=100 + +maxi_cov: + python tests/maxi_cov.py + coverage html + coverage report --show-missing --fail-under=100 + +build: + python setup.py build_ext --inplace + +clean: + find . -name '*.pyc' -o -name '*.so' -delete + find . -name __pycache__ -delete + rm -rf .coverage .mypy_cache build compliance/reports dist docs/_build htmlcov MANIFEST src/websockets.egg-info diff --git a/tests/wpt/tests/tools/third_party/websockets/PKG-INFO b/tests/wpt/tests/tools/third_party/websockets/PKG-INFO deleted file mode 100644 index 3b042a3f9f8..00000000000 --- a/tests/wpt/tests/tools/third_party/websockets/PKG-INFO +++ /dev/null @@ -1,174 +0,0 @@ -Metadata-Version: 2.1 -Name: websockets -Version: 10.3 -Summary: An implementation of the WebSocket Protocol (RFC 6455 & 7692) -Home-page: https://github.com/aaugustin/websockets -Author: Aymeric Augustin -Author-email: aymeric.augustin@m4x.org -License: BSD -Project-URL: Changelog, https://websockets.readthedocs.io/en/stable/project/changelog.html -Project-URL: Documentation, https://websockets.readthedocs.io/ -Project-URL: Funding, https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=readme -Project-URL: Tracker, https://github.com/aaugustin/websockets/issues -Platform: UNKNOWN -Classifier: Development Status :: 5 - Production/Stable -Classifier: Environment :: Web Environment -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: BSD License -Classifier: Operating System :: OS Independent -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 -Requires-Python: >=3.7 -License-File: LICENSE - -.. image:: logo/horizontal.svg - :width: 480px - :alt: websockets - -|licence| |version| |pyversions| |wheel| |tests| |docs| - -.. |licence| image:: https://img.shields.io/pypi/l/websockets.svg - :target: https://pypi.python.org/pypi/websockets - -.. |version| image:: https://img.shields.io/pypi/v/websockets.svg - :target: https://pypi.python.org/pypi/websockets - -.. |pyversions| image:: https://img.shields.io/pypi/pyversions/websockets.svg - :target: https://pypi.python.org/pypi/websockets - -.. |wheel| image:: https://img.shields.io/pypi/wheel/websockets.svg - :target: https://pypi.python.org/pypi/websockets - -.. |tests| image:: https://img.shields.io/github/checks-status/aaugustin/websockets/main - :target: https://github.com/aaugustin/websockets/actions/workflows/tests.yml - -.. |docs| image:: https://img.shields.io/readthedocs/websockets.svg - :target: https://websockets.readthedocs.io/ - -What is ``websockets``? ------------------------ - -websockets is a library for building WebSocket_ servers and clients in Python -with a focus on correctness, simplicity, robustness, and performance. - -.. _WebSocket: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API - -Built on top of ``asyncio``, Python's standard asynchronous I/O framework, it -provides an elegant coroutine-based API. - -`Documentation is available on Read the Docs. <https://websockets.readthedocs.io/>`_ - -Here's how a client sends and receives messages: - -.. copy-pasted because GitHub doesn't support the include directive - -.. code:: python - - #!/usr/bin/env python - - import asyncio - from websockets import connect - - async def hello(uri): - async with connect(uri) as websocket: - await websocket.send("Hello world!") - await websocket.recv() - - asyncio.run(hello("ws://localhost:8765")) - -And here's an echo server: - -.. code:: python - - #!/usr/bin/env python - - import asyncio - from websockets import serve - - async def echo(websocket): - async for message in websocket: - await websocket.send(message) - - async def main(): - async with serve(echo, "localhost", 8765): - await asyncio.Future() # run forever - - asyncio.run(main()) - -Does that look good? - -`Get started with the tutorial! <https://websockets.readthedocs.io/en/stable/intro/index.html>`_ - -Why should I use ``websockets``? --------------------------------- - -The development of ``websockets`` is shaped by four principles: - -1. **Correctness**: ``websockets`` is heavily tested for compliance - with :rfc:`6455`. Continuous integration fails under 100% branch - coverage. - -2. **Simplicity**: all you need to understand is ``msg = await ws.recv()`` and - ``await ws.send(msg)``. ``websockets`` takes care of managing connections - so you can focus on your application. - -3. **Robustness**: ``websockets`` is built for production. For example, it was - the only library to `handle backpressure correctly`_ before the issue - became widely known in the Python community. - -4. **Performance**: memory usage is optimized and configurable. A C extension - accelerates expensive operations. It's pre-compiled for Linux, macOS and - Windows and packaged in the wheel format for each system and Python version. - -Documentation is a first class concern in the project. Head over to `Read the -Docs`_ and see for yourself. - -.. _Read the Docs: https://websockets.readthedocs.io/ -.. _handle backpressure correctly: https://vorpus.org/blog/some-thoughts-on-asynchronous-api-design-in-a-post-asyncawait-world/#websocket-servers - -Why shouldn't I use ``websockets``? ------------------------------------ - -* If you prefer callbacks over coroutines: ``websockets`` was created to - provide the best coroutine-based API to manage WebSocket connections in - Python. Pick another library for a callback-based API. - -* If you're looking for a mixed HTTP / WebSocket library: ``websockets`` aims - at being an excellent implementation of :rfc:`6455`: The WebSocket Protocol - and :rfc:`7692`: Compression Extensions for WebSocket. Its support for HTTP - is minimal — just enough for a HTTP health check. - - If you want to do both in the same server, look at HTTP frameworks that - build on top of ``websockets`` to support WebSocket connections, like - Sanic_. - -.. _Sanic: https://sanicframework.org/en/ - -What else? ----------- - -Bug reports, patches and suggestions are welcome! - -To report a security vulnerability, please use the `Tidelift security -contact`_. Tidelift will coordinate the fix and disclosure. - -.. _Tidelift security contact: https://tidelift.com/security - -For anything else, please open an issue_ or send a `pull request`_. - -.. _issue: https://github.com/aaugustin/websockets/issues/new -.. _pull request: https://github.com/aaugustin/websockets/compare/ - -Participants must uphold the `Contributor Covenant code of conduct`_. - -.. _Contributor Covenant code of conduct: https://github.com/aaugustin/websockets/blob/main/CODE_OF_CONDUCT.md - -``websockets`` is released under the `BSD license`_. - -.. _BSD license: https://github.com/aaugustin/websockets/blob/main/LICENSE - - diff --git a/tests/wpt/tests/tools/third_party/websockets/README.rst b/tests/wpt/tests/tools/third_party/websockets/README.rst index 2b9a445ea5c..870b208baae 100644 --- a/tests/wpt/tests/tools/third_party/websockets/README.rst +++ b/tests/wpt/tests/tools/third_party/websockets/README.rst @@ -2,7 +2,7 @@ :width: 480px :alt: websockets -|licence| |version| |pyversions| |wheel| |tests| |docs| +|licence| |version| |pyversions| |tests| |docs| |openssf| .. |licence| image:: https://img.shields.io/pypi/l/websockets.svg :target: https://pypi.python.org/pypi/websockets @@ -13,15 +13,15 @@ .. |pyversions| image:: https://img.shields.io/pypi/pyversions/websockets.svg :target: https://pypi.python.org/pypi/websockets -.. |wheel| image:: https://img.shields.io/pypi/wheel/websockets.svg - :target: https://pypi.python.org/pypi/websockets - -.. |tests| image:: https://img.shields.io/github/checks-status/aaugustin/websockets/main - :target: https://github.com/aaugustin/websockets/actions/workflows/tests.yml +.. |tests| image:: https://img.shields.io/github/checks-status/python-websockets/websockets/main?label=tests + :target: https://github.com/python-websockets/websockets/actions/workflows/tests.yml .. |docs| image:: https://img.shields.io/readthedocs/websockets.svg :target: https://websockets.readthedocs.io/ +.. |openssf| image:: https://bestpractices.coreinfrastructure.org/projects/6475/badge + :target: https://bestpractices.coreinfrastructure.org/projects/6475 + What is ``websockets``? ----------------------- @@ -30,37 +30,24 @@ with a focus on correctness, simplicity, robustness, and performance. .. _WebSocket: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API -Built on top of ``asyncio``, Python's standard asynchronous I/O framework, it -provides an elegant coroutine-based API. +Built on top of ``asyncio``, Python's standard asynchronous I/O framework, the +default implementation provides an elegant coroutine-based API. -`Documentation is available on Read the Docs. <https://websockets.readthedocs.io/>`_ +An implementation on top of ``threading`` and a Sans-I/O implementation are also +available. -Here's how a client sends and receives messages: +`Documentation is available on Read the Docs. <https://websockets.readthedocs.io/>`_ .. copy-pasted because GitHub doesn't support the include directive -.. code:: python - - #!/usr/bin/env python - - import asyncio - from websockets import connect - - async def hello(uri): - async with connect(uri) as websocket: - await websocket.send("Hello world!") - await websocket.recv() - - asyncio.run(hello("ws://localhost:8765")) - -And here's an echo server: +Here's an echo server with the ``asyncio`` API: .. code:: python #!/usr/bin/env python import asyncio - from websockets import serve + from websockets.server import serve async def echo(websocket): async for message in websocket: @@ -72,6 +59,23 @@ And here's an echo server: asyncio.run(main()) +Here's how a client sends and receives messages with the ``threading`` API: + +.. code:: python + + #!/usr/bin/env python + + from websockets.sync.client import connect + + def hello(): + with connect("ws://localhost:8765") as websocket: + websocket.send("Hello world!") + message = websocket.recv() + print(f"Received: {message}") + + hello() + + Does that look good? `Get started with the tutorial! <https://websockets.readthedocs.io/en/stable/intro/index.html>`_ @@ -79,7 +83,7 @@ Does that look good? .. raw:: html <hr> - <img align="left" height="150" width="150" src="https://raw.githubusercontent.com/aaugustin/websockets/main/logo/tidelift.png"> + <img align="left" height="150" width="150" src="https://raw.githubusercontent.com/python-websockets/websockets/main/logo/tidelift.png"> <h3 align="center"><i>websockets for enterprise</i></h3> <p align="center"><i>Available as part of the Tidelift Subscription</i></p> <p align="center"><i>The maintainers of websockets and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. <a href="https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=readme">Learn more.</a></i></p> @@ -91,9 +95,8 @@ Why should I use ``websockets``? The development of ``websockets`` is shaped by four principles: -1. **Correctness**: ``websockets`` is heavily tested for compliance - with :rfc:`6455`. Continuous integration fails under 100% branch - coverage. +1. **Correctness**: ``websockets`` is heavily tested for compliance with + :rfc:`6455`. Continuous integration fails under 100% branch coverage. 2. **Simplicity**: all you need to understand is ``msg = await ws.recv()`` and ``await ws.send(msg)``. ``websockets`` takes care of managing connections @@ -123,7 +126,7 @@ Why shouldn't I use ``websockets``? * If you're looking for a mixed HTTP / WebSocket library: ``websockets`` aims at being an excellent implementation of :rfc:`6455`: The WebSocket Protocol and :rfc:`7692`: Compression Extensions for WebSocket. Its support for HTTP - is minimal — just enough for a HTTP health check. + is minimal — just enough for an HTTP health check. If you want to do both in the same server, look at HTTP frameworks that build on top of ``websockets`` to support WebSocket connections, like @@ -143,13 +146,13 @@ contact`_. Tidelift will coordinate the fix and disclosure. For anything else, please open an issue_ or send a `pull request`_. -.. _issue: https://github.com/aaugustin/websockets/issues/new -.. _pull request: https://github.com/aaugustin/websockets/compare/ +.. _issue: https://github.com/python-websockets/websockets/issues/new +.. _pull request: https://github.com/python-websockets/websockets/compare/ Participants must uphold the `Contributor Covenant code of conduct`_. -.. _Contributor Covenant code of conduct: https://github.com/aaugustin/websockets/blob/main/CODE_OF_CONDUCT.md +.. _Contributor Covenant code of conduct: https://github.com/python-websockets/websockets/blob/main/CODE_OF_CONDUCT.md ``websockets`` is released under the `BSD license`_. -.. _BSD license: https://github.com/aaugustin/websockets/blob/main/LICENSE +.. _BSD license: https://github.com/python-websockets/websockets/blob/main/LICENSE diff --git a/tests/wpt/tests/tools/third_party/websockets/SECURITY.md b/tests/wpt/tests/tools/third_party/websockets/SECURITY.md new file mode 100644 index 00000000000..175b20c589d --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/SECURITY.md @@ -0,0 +1,12 @@ +# Security + +## Policy + +Only the latest version receives security updates. + +## Contact information + +Please report security vulnerabilities to the +[Tidelift security team](https://tidelift.com/security). + +Tidelift will coordinate the fix and disclosure. diff --git a/tests/wpt/tests/tools/third_party/websockets/compliance/README.rst b/tests/wpt/tests/tools/third_party/websockets/compliance/README.rst new file mode 100644 index 00000000000..8570f9176d5 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/compliance/README.rst @@ -0,0 +1,50 @@ +Autobahn Testsuite +================== + +General information and installation instructions are available at +https://github.com/crossbario/autobahn-testsuite. + +To improve performance, you should compile the C extension first:: + + $ python setup.py build_ext --inplace + +Running the test suite +---------------------- + +All commands below must be run from the directory containing this file. + +To test the server:: + + $ PYTHONPATH=.. python test_server.py + $ wstest -m fuzzingclient + +To test the client:: + + $ wstest -m fuzzingserver + $ PYTHONPATH=.. python test_client.py + +Run the first command in a shell. Run the second command in another shell. +It should take about ten minutes to complete — wstest is the bottleneck. +Then kill the first one with Ctrl-C. + +The test client or server shouldn't display any exceptions. The results are +stored in reports/clients/index.html. + +Note that the Autobahn software only supports Python 2, while ``websockets`` +only supports Python 3; you need two different environments. + +Conformance notes +----------------- + +Some test cases are more strict than the RFC. Given the implementation of the +library and the test echo client or server, ``websockets`` gets a "Non-Strict" +in these cases. + +In 3.2, 3.3, 4.1.3, 4.1.4, 4.2.3, 4.2.4, and 5.15 ``websockets`` notices the +protocol error and closes the connection before it has had a chance to echo +the previous frame. + +In 6.4.3 and 6.4.4, even though it uses an incremental decoder, ``websockets`` +doesn't notice the invalid utf-8 fast enough to get a "Strict" pass. These +tests are more strict than the RFC. + diff --git a/tests/wpt/tests/tools/third_party/websockets/compliance/fuzzingclient.json b/tests/wpt/tests/tools/third_party/websockets/compliance/fuzzingclient.json new file mode 100644 index 00000000000..202ff49a03a --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/compliance/fuzzingclient.json @@ -0,0 +1,11 @@ + +{ + "options": {"failByDrop": false}, + "outdir": "./reports/servers", + + "servers": [{"agent": "websockets", "url": "ws://localhost:8642", "options": {"version": 18}}], + + "cases": ["*"], + "exclude-cases": [], + "exclude-agent-cases": {} +} diff --git a/tests/wpt/tests/tools/third_party/websockets/compliance/fuzzingserver.json b/tests/wpt/tests/tools/third_party/websockets/compliance/fuzzingserver.json new file mode 100644 index 00000000000..1bdb42723ef --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/compliance/fuzzingserver.json @@ -0,0 +1,12 @@ + +{ + "url": "ws://localhost:8642", + + "options": {"failByDrop": false}, + "outdir": "./reports/clients", + "webport": 8080, + + "cases": ["*"], + "exclude-cases": [], + "exclude-agent-cases": {} +} diff --git a/tests/wpt/tests/tools/third_party/websockets/compliance/test_client.py b/tests/wpt/tests/tools/third_party/websockets/compliance/test_client.py new file mode 100644 index 00000000000..1ed4d711e92 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/compliance/test_client.py @@ -0,0 +1,48 @@ +import json +import logging +import urllib.parse + +import asyncio +import websockets + + +logging.basicConfig(level=logging.WARNING) + +# Uncomment this line to make only websockets more verbose. +# logging.getLogger('websockets').setLevel(logging.DEBUG) + + +SERVER = "ws://127.0.0.1:8642" +AGENT = "websockets" + + +async def get_case_count(server): + uri = f"{server}/getCaseCount" + async with websockets.connect(uri) as ws: + msg = ws.recv() + return json.loads(msg) + + +async def run_case(server, case, agent): + uri = f"{server}/runCase?case={case}&agent={agent}" + async with websockets.connect(uri, max_size=2 ** 25, max_queue=1) as ws: + async for msg in ws: + await ws.send(msg) + + +async def update_reports(server, agent): + uri = f"{server}/updateReports?agent={agent}" + async with websockets.connect(uri): + pass + + +async def run_tests(server, agent): + cases = await get_case_count(server) + for case in range(1, cases + 1): + print(f"Running test case {case} out of {cases}", end="\r") + await run_case(server, case, agent) + print(f"Ran {cases} test cases ") + await update_reports(server, agent) + + +asyncio.run(run_tests(SERVER, urllib.parse.quote(AGENT))) diff --git a/tests/wpt/tests/tools/third_party/websockets/compliance/test_server.py b/tests/wpt/tests/tools/third_party/websockets/compliance/test_server.py new file mode 100644 index 00000000000..92f895d9269 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/compliance/test_server.py @@ -0,0 +1,29 @@ +import logging + +import asyncio +import websockets + + +logging.basicConfig(level=logging.WARNING) + +# Uncomment this line to make only websockets more verbose. +# logging.getLogger('websockets').setLevel(logging.DEBUG) + + +HOST, PORT = "127.0.0.1", 8642 + + +async def echo(ws): + async for msg in ws: + await ws.send(msg) + + +async def main(): + with websockets.serve(echo, HOST, PORT, max_size=2 ** 25, max_queue=1): + try: + await asyncio.Future() + except KeyboardInterrupt: + pass + + +asyncio.run(main()) diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/Makefile b/tests/wpt/tests/tools/third_party/websockets/docs/Makefile new file mode 100644 index 00000000000..04587064588 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/Makefile @@ -0,0 +1,23 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +livehtml: + sphinx-autobuild --watch "$(SOURCEDIR)/../src" "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/_static/favicon.ico b/tests/wpt/tests/tools/third_party/websockets/docs/_static/favicon.ico Binary files differnew file mode 100644 index 00000000000..36e855029d7 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/_static/favicon.ico diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/_static/tidelift.png b/tests/wpt/tests/tools/third_party/websockets/docs/_static/tidelift.png Binary files differnew file mode 100644 index 00000000000..317dc4d9852 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/_static/tidelift.png diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/_static/websockets.svg b/tests/wpt/tests/tools/third_party/websockets/docs/_static/websockets.svg new file mode 100644 index 00000000000..b07fb223873 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/_static/websockets.svg @@ -0,0 +1,31 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="480" height="320" viewBox="0 0 480 320"> + <linearGradient id="w" x1="0.2333" y1="0" x2="0.5889" y2="0.5333"> + <stop offset="0%" stop-color="#ffe873" /> + <stop offset="100%" stop-color="#ffd43b" /> + </linearGradient> + <linearGradient id="s" x1="0.2333" y1="0" x2="0.5889" y2="0.5333"> + <stop offset="0%" stop-color="#5a9fd4" /> + <stop offset="100%" stop-color="#306998" /> + </linearGradient> + <g> + <path fill="url(#w)" d="m 263.40708,146.81618 c -0.43704,0.0747 -0.88656,0.12978 -1.35572,0.14933 -2.45813,0.0764 -4.25357,-0.58665 -5.82335,-2.15107 l -8.89246,-8.85942 -11.23464,-11.19805 -36.04076,-35.919454 c -3.43568,-3.42217 -7.33248,-5.347474 -11.58962,-5.723468 -2.22981,-0.198219 -4.47388,0.03111 -6.64036,0.675545 -3.24213,0.944875 -6.13552,2.664848 -8.59366,5.116366 -3.83437,3.819499 -5.86349,8.414979 -5.87598,13.287801 -0.0607,4.95281 1.95153,9.60074 5.8082,13.44424 l 55.62289,55.43648 c 1.82219,1.84175 2.65971,3.79549 2.63384,6.14568 l 0.004,0.208 c 0.0527,2.43196 -0.75991,4.34571 -2.6267,6.20612 -1.78028,1.77598 -3.8094,2.65241 -6.30945,2.75552 -2.45814,0.0764 -4.25446,-0.58844 -5.82514,-2.15286 L 160.50255,128.2618 c -5.21417,-5.19459 -11.7029,-6.98745 -18.22998,-5.04881 -3.2457,0.9431 -6.13553,2.66307 -8.59545,5.11459 -3.83437,3.82127 -5.86527,8.41676 -5.87597,13.28957 -0.0562,4.95281 1.95152,9.60252 5.80641,13.4478 l 58.10689,57.90577 c 8.31984,8.29143 19.34042,11.9376 32.74331,10.83806 12.57967,-1.02043 23.02317,-5.5848 31.03441,-13.57313 7.51265,-7.4861 11.96423,-16.35175 13.28695,-26.42537 10.47206,-1.68264 19.29494,-6.04524 26.27512,-13.00158 4.01364,-3.99994 7.14963,-8.3972 9.40531,-13.16157 -14.15569,-0.39911 -28.23645,-4.00972 -41.05247,-10.83095 z" /> + <path fill="url(#s)" d="m 308.76038,138.11854 c 0.10259,-12.84514 -4.43017,-23.98541 -13.50635,-33.1346 L 259.37292,69.225372 c -0.24349,-0.240885 -0.46469,-0.487992 -0.68678,-0.744877 -1.48416,-1.739529 -2.18788,-3.583056 -2.21018,-5.807022 -0.0259,-2.470184 0.84911,-4.508375 2.7605,-6.407902 1.91406,-1.909304 3.8531,-2.737735 6.36564,-2.684403 2.53662,0.024 4.62728,0.943097 6.57257,2.881734 l 60.59178,60.384848 12.11408,-12.06914 c 1.12203,-0.90755 1.95777,-1.76887 2.87823,-2.93418 5.91879,-7.515442 5.26947,-18.272611 -1.51003,-25.028952 L 299.00456,29.727312 c -9.19393,-9.157192 -20.36703,-13.776677 -33.16789,-13.7269 -12.94266,-0.05067 -24.14163,4.548375 -33.28739,13.662901 -9.02892,8.996307 -13.64015,19.93925 -13.7008,32.487501 l -0.004,0.14222 c -0.002,0.167998 -0.005,0.336884 -0.005,0.506659 -0.091,12.232701 4.10729,22.95787 12.48154,31.881285 0.40226,0.43022 0.80274,0.85777 1.22283,1.27821 l 35.75088,35.626122 c 1.88909,1.88174 2.71769,3.79638 2.69361,6.20968 l 0.003,0.20977 c 0.0527,2.43197 -0.76081,4.34571 -2.6276,6.20791 -1.44759,1.43909 -3.06286,2.27818 -4.9564,2.60262 12.81601,6.82123 26.89677,10.43184 41.05246,10.83362 2.80598,-5.92525 4.2509,-12.41848 4.29906,-19.43526 z" /> + <path fill="#ffffff" d="m 327.48093,85.181572 c 2.84701,-2.838179 7.46359,-2.836401 10.30883,0 2.84433,2.834623 2.84612,7.435446 -0.002,10.270956 -2.84345,2.83818 -7.46271,2.83818 -10.30704,0 -2.84524,-2.83551 -2.84791,-7.435444 0,-10.270956 z" /> + </g> + <g> + <g fill="#ffd43b"> + <path d="m 25.719398,284.91839 c 0,2.59075 0.912299,4.79875 2.736898,6.62269 1.824599,1.82657 4.033255,2.73821 6.625313,2.73821 2.591402,0 4.800058,-0.91164 6.624002,-2.73821 1.825254,-1.82394 2.738209,-4.03194 2.738209,-6.62269 v -21.77984 c 0,-1.32126 0.475811,-2.45901 1.42809,-3.40998 0.952278,-0.95359 2.089375,-1.43006 3.411947,-1.43006 h 0.0793 c 1.348132,0 2.471467,0.47647 3.371969,1.43006 0.952278,0.95097 1.428745,2.08938 1.428745,3.40998 v 21.77984 c 0,2.59075 0.912299,4.79875 2.738209,6.62269 1.823944,1.82657 4.031289,2.73821 6.624002,2.73821 2.618274,0 4.839382,-0.91164 6.663981,-2.73821 1.825254,-1.82394 2.738209,-4.03194 2.738209,-6.62269 v -21.77984 c 0,-1.32126 0.475156,-2.45901 1.42809,-3.40998 0.897881,-0.95359 2.022526,-1.43006 3.371969,-1.43006 h 0.07865 c 1.323228,0 2.460325,0.47647 3.411948,1.43006 0.926062,0.95097 1.388766,2.08938 1.388766,3.40998 v 21.77984 c 0,5.26211 -1.865233,9.75807 -5.593077,13.48657 -3.729156,3.7285 -8.22577,5.59373 -13.487876,5.59373 -6.294998,0 -11.028207,-2.08807 -14.202904,-6.26747 -3.199602,4.1794 -7.94723,6.26747 -14.240916,6.26747 -5.262763,0 -9.759377,-1.86523 -13.487876,-5.59373 C 17.866544,294.67646 16,290.18115 16,284.91839 v -21.77984 c 0,-1.32126 0.476467,-2.45901 1.428745,-3.40998 0.951623,-0.95359 2.075612,-1.43006 3.371969,-1.43006 h 0.11928 c 1.295702,0 2.419036,0.47647 3.372625,1.43006 0.950967,0.95097 1.427434,2.08938 1.427434,3.40998 v 21.77984 z" /> + <path d="m 132.94801,271.6291 c 0.31786,0.66063 0.47712,1.33371 0.47712,2.02252 0,0.55577 -0.10551,1.11089 -0.3172,1.66665 -0.45026,1.24262 -1.29636,2.14181 -2.53898,2.69692 -3.70293,1.66665 -8.56853,3.8622 -14.59875,6.58599 -7.48453,3.38442 -11.87497,5.38139 -13.17067,5.9909 2.00942,2.53832 5.14414,3.80715 9.40219,3.80715 2.82931,0 5.39515,-0.83234 7.69556,-2.499 2.24798,-1.63977 3.82222,-3.75537 4.72141,-6.34808 0.76746,-2.16868 2.30107,-3.25269 4.60148,-3.25269 1.63912,0 2.94859,0.68881 3.92708,2.06185 0.6082,0.84742 0.9123,1.7335 0.9123,2.65891 0,0.55577 -0.10552,1.12399 -0.31655,1.70532 -1.56048,4.52348 -4.29869,8.17334 -8.21135,10.95087 -3.96706,2.88108 -8.41059,4.32293 -13.32993,4.32293 -6.29434,0 -11.67639,-2.23356 -16.145474,-6.70395 -4.469743,-4.46975 -6.704615,-9.85114 -6.704615,-16.14679 0,-6.29434 2.234872,-11.67507 6.704615,-16.14678 4.468434,-4.46843 9.851134,-6.70396 16.145474,-6.70396 4.54773,0 8.70027,1.24392 12.45629,3.7285 3.72785,2.43607 6.49162,5.63437 8.29,9.60274 z m -20.74695,-3.5332 c -3.64985,0 -6.7577,1.28391 -9.32289,3.84909 -2.53897,2.5665 -3.808452,5.67435 -3.808452,9.32289 v 0.27789 l 22.175692,-9.95731 c -1.95633,-2.32597 -4.97177,-3.49256 -9.04435,-3.49256 z" /> + <path d="m 146.11999,242.03442 c 1.2957,0 2.41904,0.46336 3.37197,1.38876 0.95228,0.95097 1.42874,2.08938 1.42874,3.4113 v 15.4311 c 2.98792,-2.64318 7.36525,-3.96707 13.13004,-3.96707 6.29434,0 11.67638,2.23488 16.14613,6.70396 4.46908,4.47106 6.70461,9.85245 6.70461,16.14679 0,6.29499 -2.23553,11.67638 -6.70461,16.14678 -4.46909,4.4704 -9.85113,6.70396 -16.14613,6.70396 -6.295,0 -11.66262,-2.22111 -16.10549,-6.66529 -4.4704,-4.41469 -6.71838,-9.77052 -6.7446,-16.06617 v -34.43341 c 0,-1.32257 0.47647,-2.46032 1.42875,-3.41129 0.95162,-0.92541 2.07561,-1.38877 3.37197,-1.38877 h 0.11862 z m 17.93009,26.06148 c -3.64919,0 -6.75704,1.28391 -9.32288,3.84909 -2.53767,2.5665 -3.80781,5.67435 -3.80781,9.32289 0,3.62364 1.27014,6.71772 3.80781,9.28291 2.56584,2.56519 5.67303,3.84778 9.32288,3.84778 3.62364,0 6.71773,-1.28259 9.28357,-3.84778 2.56387,-2.56519 3.84712,-5.65927 3.84712,-9.28291 0,-3.64788 -1.28325,-6.75639 -3.84712,-9.32289 -2.56584,-2.56518 -5.65927,-3.84909 -9.28357,-3.84909 z" /> + </g> + <g fill="#306998"> + <path d="m 205.94246,268.01922 c -1.16397,0 -2.14247,0.39586 -2.93548,1.18888 -0.79368,0.82054 -1.19019,1.79838 -1.19019,2.93548 0,1.58735 0.76681,2.77753 2.30172,3.56989 0.52825,0.29165 2.7369,0.95228 6.62466,1.98386 3.14717,0.89985 5.48691,2.07627 7.02051,3.53057 2.19621,2.09003 3.29267,5.06549 3.29267,8.92704 0,3.80714 -1.34879,7.0736 -4.04571,9.79739 -2.72444,2.69823 -5.9909,4.04636 -9.7987,4.04636 h -10.35381 c -1.29701,0 -2.41969,-0.47516 -3.37262,-1.42875 -0.95228,-0.89853 -1.42875,-2.02252 -1.42875,-3.37065 v -0.0806 c 0,-1.32126 0.47647,-2.45901 1.42875,-3.41129 0.95227,-0.95228 2.07561,-1.42874 3.37262,-1.42874 h 10.75032 c 1.16331,0 2.14246,-0.39586 2.93548,-1.18888 0.79368,-0.79367 1.19019,-1.77151 1.19019,-2.93548 0,-1.45561 -0.7537,-2.55339 -2.26044,-3.29201 -0.3965,-0.18678 -2.61892,-0.84742 -6.66529,-1.98386 -3.14782,-0.9254 -5.48887,-2.14377 -7.02247,-3.65051 -2.19555,-2.1418 -3.29202,-5.17035 -3.29202,-9.08432 0,-3.80846 1.34945,-7.06049 4.04702,-9.75807 2.72314,-2.72379 5.99024,-4.087 9.79805,-4.087 h 7.2997 c 1.32192,0 2.45967,0.47647 3.41195,1.43006 0.95162,0.95097 1.42809,2.08938 1.42809,3.40998 v 0.0793 c 0,1.34945 -0.47647,2.47409 -1.42809,3.37263 -0.95228,0.95097 -2.09003,1.42874 -3.41195,1.42874 z" /> + <path d="m 249.06434,258.29851 c 6.29434,0 11.67573,2.23488 16.14612,6.70396 4.46909,4.47106 6.70396,9.85245 6.70396,16.14679 0,6.29499 -2.23487,11.67638 -6.70396,16.14678 -4.46974,4.46974 -9.85178,6.70396 -16.14612,6.70396 -6.29435,0 -11.67639,-2.23356 -16.14548,-6.70396 -4.46974,-4.46974 -6.70461,-9.85113 -6.70461,-16.14678 0,-6.29434 2.23487,-11.67508 6.70461,-16.14679 4.46909,-4.46908 9.85113,-6.70396 16.14548,-6.70396 z m 0,9.79739 c -3.64986,0 -6.7577,1.28391 -9.32289,3.84909 -2.53963,2.5665 -3.80911,5.67435 -3.80911,9.32289 0,3.62364 1.26948,6.71772 3.80911,9.28291 2.56519,2.56519 5.67238,3.84778 9.32289,3.84778 3.62298,0 6.71706,-1.28259 9.28291,-3.84778 2.56518,-2.56519 3.84778,-5.65927 3.84778,-9.28291 0,-3.64788 -1.2826,-6.75639 -3.84778,-9.32289 -2.56585,-2.56518 -5.65928,-3.84909 -9.28291,-3.84909 z" /> + <path d="m 307.22146,259.37007 c 2.24864,0.71438 3.37263,2.24798 3.37263,4.60148 v 0.19989 c 0,1.6116 -0.64884,2.89419 -1.94454,3.84778 -0.89919,0.63376 -1.82525,0.95097 -2.77622,0.95097 -0.50334,0 -1.01913,-0.0793 -1.54737,-0.23791 -1.29636,-0.42272 -2.63204,-0.63638 -4.00638,-0.63638 -3.64986,0 -6.75836,1.28391 -9.32289,3.84909 -2.53963,2.5665 -3.80846,5.67435 -3.80846,9.32289 0,3.62364 1.26883,6.71772 3.80846,9.28291 2.56453,2.56519 5.67238,3.84778 9.32289,3.84778 1.375,0 2.71068,-0.21103 4.00638,-0.63507 0.50203,-0.1586 1.00471,-0.2379 1.50739,-0.2379 0.97718,0 1.91767,0.31851 2.81686,0.95358 1.2957,0.95097 1.94453,2.24798 1.94453,3.88776 0,2.32728 -1.12464,3.86089 -3.37262,4.60148 -2.22111,0.6875 -4.52152,1.03027 -6.90189,1.03027 -6.29434,0 -11.67638,-2.23356 -16.14678,-6.70396 -4.46843,-4.46974 -6.70396,-9.85113 -6.70396,-16.14678 0,-6.29435 2.23487,-11.67508 6.70396,-16.14679 4.46974,-4.46843 9.85178,-6.70396 16.14678,-6.70396 2.37906,0.001 4.68012,0.35981 6.90123,1.07287 z" /> + <path d="m 322.25671,242.03442 c 1.29504,0 2.41903,0.46336 3.37262,1.38876 0.95163,0.95097 1.42809,2.08938 1.42809,3.4113 v 27.49154 h 1.50739 c 3.38508,0 6.33301,-1.12399 8.84708,-3.37263 2.45901,-2.24798 3.86023,-5.0242 4.20431,-8.33063 0.15861,-1.24261 0.68816,-2.26174 1.58735,-3.0541 0.89854,-0.84611 1.96944,-1.27015 3.21271,-1.27015 h 0.11863 c 1.40252,0 2.5796,0.53021 3.53122,1.58735 0.84676,0.92541 1.26949,1.99697 1.26949,3.21271 0,0.15861 -0.0138,0.33163 -0.0393,0.51579 -0.63507,6.63842 -3.17405,11.61019 -7.61692,14.91531 2.32663,1.43006 4.46909,3.84909 6.42739,7.26039 2.03563,3.51746 3.05476,7.31412 3.05476,11.38473 v 2.02515 c 0,1.34813 -0.47712,2.47147 -1.42809,3.37066 -0.95359,0.95359 -2.07692,1.42874 -3.37263,1.42874 h -0.11928 c -1.29635,0 -2.41969,-0.47515 -3.37196,-1.42874 -0.95228,-0.89854 -1.42809,-2.02253 -1.42809,-3.37066 v -2.02515 c -0.0275,-3.59414 -1.31012,-6.67708 -3.84844,-9.24358 -2.56584,-2.53832 -5.66058,-3.80715 -9.28291,-3.80715 h -3.25269 v 15.07523 c 0,1.34813 -0.47646,2.47146 -1.42809,3.37065 -0.95293,0.95359 -2.07758,1.42875 -3.37262,1.42875 h -0.12059 c -1.2957,0 -2.41838,-0.47516 -3.37132,-1.42875 -0.95162,-0.89853 -1.42809,-2.02252 -1.42809,-3.37065 v -52.36547 c 0,-1.32257 0.47647,-2.46032 1.42809,-3.41129 0.95228,-0.92541 2.07562,-1.38877 3.37132,-1.38877 h 0.12059 z" /> + <path d="m 402.31164,271.6291 c 0.31721,0.66063 0.47581,1.33371 0.47581,2.02252 0,0.55577 -0.10617,1.11089 -0.31655,1.66665 -0.45025,1.24262 -1.29635,2.14181 -2.53897,2.69692 -3.70294,1.66665 -8.56919,3.8622 -14.59876,6.58599 -7.48452,3.38442 -11.87496,5.38139 -13.17067,5.9909 2.00877,2.53832 5.14349,3.80715 9.40219,3.80715 2.82866,0 5.3945,-0.83234 7.69622,-2.499 2.24732,-1.63977 3.82091,-3.75537 4.7201,-6.34808 0.76681,-2.16868 2.30172,-3.25269 4.60148,-3.25269 1.63978,0 2.94924,0.68881 3.92839,2.06185 0.60689,0.84742 0.91165,1.7335 0.91165,2.65891 0,0.55577 -0.10552,1.12399 -0.31721,1.70532 -1.56048,4.52348 -4.29738,8.17334 -8.21135,10.95087 -3.96706,2.88108 -8.40994,4.32293 -13.32928,4.32293 -6.29434,0 -11.67638,-2.23356 -16.14547,-6.70395 -4.46974,-4.46975 -6.70461,-9.85114 -6.70461,-16.14679 0,-6.29434 2.23487,-11.67507 6.70461,-16.14678 4.46843,-4.46843 9.85113,-6.70396 16.14547,-6.70396 4.54774,0 8.70093,1.24392 12.4563,3.7285 3.7285,2.43607 6.49161,5.63437 8.29065,9.60274 z m -20.7476,-3.5332 c -3.6492,0 -6.7577,1.28391 -9.32289,3.84909 -2.53897,2.5665 -3.80846,5.67435 -3.80846,9.32289 v 0.27789 l 22.1757,-9.95731 c -1.95699,-2.32597 -4.97177,-3.49256 -9.04435,-3.49256 z" /> + <path d="m 415.48166,242.03442 c 1.2957,0 2.41969,0.46336 3.37262,1.38876 0.95162,0.95097 1.42809,2.08938 1.42809,3.4113 v 11.46403 h 5.95092 c 1.2957,0 2.41903,0.47647 3.37262,1.43006 0.95163,0.95097 1.42678,2.08938 1.42678,3.40998 v 0.0793 c 0,1.34945 -0.47515,2.47409 -1.42678,3.37263 -0.95293,0.95097 -2.07692,1.42874 -3.37262,1.42874 h -5.95092 v 23.52252 c 0,0.76811 0.26347,1.41695 0.79367,1.94453 0.5289,0.53021 1.19019,0.79368 1.98321,0.79368 h 3.17404 c 1.2957,0 2.41903,0.47646 3.37262,1.42874 0.95163,0.95228 1.42678,2.09003 1.42678,3.41129 v 0.0806 c 0,1.34813 -0.47515,2.47146 -1.42678,3.37065 C 428.65298,303.52484 427.52899,304 426.23329,304 h -3.17404 c -3.43817,0 -6.38675,-1.21574 -8.84642,-3.6492 -2.43411,-2.45901 -3.6492,-5.39515 -3.6492,-8.80775 v -44.70726 c 0,-1.32258 0.47581,-2.46033 1.42809,-3.4113 0.95228,-0.9254 2.07627,-1.38876 3.37197,-1.38876 h 0.11797 z" /> + <path d="m 448.88545,268.01922 c -1.16397,0 -2.14246,0.39586 -2.93548,1.18888 -0.79368,0.82054 -1.19019,1.79838 -1.19019,2.93548 0,1.58735 0.76681,2.77753 2.30042,3.56989 0.5302,0.29165 2.7382,0.95228 6.62596,1.98386 3.14652,0.89985 5.48691,2.07627 7.02117,3.53057 2.19489,2.09003 3.29267,5.06549 3.29267,8.92704 0,3.80714 -1.34945,7.0736 -4.04637,9.79739 -2.72379,2.69823 -5.99089,4.04636 -9.79869,4.04636 h -10.35382 c -1.29635,0 -2.41969,-0.47516 -3.37262,-1.42875 -0.95228,-0.89853 -1.42744,-2.02252 -1.42744,-3.37065 v -0.0806 c 0,-1.32126 0.47516,-2.45901 1.42744,-3.41129 0.95228,-0.95228 2.07627,-1.42874 3.37262,-1.42874 h 10.75032 c 1.16332,0 2.14312,-0.39586 2.93549,-1.18888 0.79367,-0.79367 1.19018,-1.77151 1.19018,-2.93548 0,-1.45561 -0.7537,-2.55339 -2.26043,-3.29201 -0.39782,-0.18678 -2.61893,-0.84742 -6.66529,-1.98386 -3.14783,-0.9254 -5.48887,-2.14377 -7.02248,-3.65051 -2.19555,-2.1418 -3.29201,-5.17035 -3.29201,-9.08432 0,-3.80846 1.34944,-7.06049 4.04701,-9.75807 2.72314,-2.72379 5.99025,-4.087 9.7987,-4.087 h 7.29906 c 1.32322,0 2.45967,0.47647 3.41129,1.43006 0.95228,0.95097 1.42809,2.08938 1.42809,3.40998 v 0.0793 c 0,1.34945 -0.47581,2.47409 -1.42809,3.37263 -0.95162,0.95097 -2.08872,1.42874 -3.41129,1.42874 z" /> + </g> + </g> +</svg> diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/conf.py b/tests/wpt/tests/tools/third_party/websockets/docs/conf.py new file mode 100644 index 00000000000..9d61dc71736 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/conf.py @@ -0,0 +1,171 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +import datetime +import importlib +import inspect +import os +import subprocess +import sys + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.join(os.path.abspath(".."), "src")) + + +# -- Project information ----------------------------------------------------- + +project = "websockets" +copyright = f"2013-{datetime.date.today().year}, Aymeric Augustin and contributors" +author = "Aymeric Augustin" + +from websockets.version import tag as version, version as release + + +# -- General configuration --------------------------------------------------- + +nitpicky = True + +nitpick_ignore = [ + # topics/design.rst discusses undocumented APIs + ("py:meth", "client.WebSocketClientProtocol.handshake"), + ("py:meth", "server.WebSocketServerProtocol.handshake"), + ("py:attr", "legacy.protocol.WebSocketCommonProtocol.is_client"), + ("py:attr", "legacy.protocol.WebSocketCommonProtocol.messages"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.close_connection"), + ("py:attr", "legacy.protocol.WebSocketCommonProtocol.close_connection_task"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.keepalive_ping"), + ("py:attr", "legacy.protocol.WebSocketCommonProtocol.keepalive_ping_task"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.transfer_data"), + ("py:attr", "legacy.protocol.WebSocketCommonProtocol.transfer_data_task"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.connection_open"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.ensure_open"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.fail_connection"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.connection_lost"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.read_message"), + ("py:meth", "legacy.protocol.WebSocketCommonProtocol.write_frame"), +] + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.linkcode", + "sphinx.ext.napoleon", + "sphinx_copybutton", + "sphinx_inline_tabs", + "sphinxcontrib.spelling", + "sphinxcontrib_trio", + "sphinxext.opengraph", +] +# It is currently inconvenient to install PyEnchant on Apple Silicon. +try: + import sphinxcontrib.spelling +except ImportError: + extensions.remove("sphinxcontrib.spelling") + +autodoc_typehints = "description" + +autodoc_typehints_description_target = "documented" + +# Workaround for https://github.com/sphinx-doc/sphinx/issues/9560 +from sphinx.domains.python import PythonDomain + +assert PythonDomain.object_types["data"].roles == ("data", "obj") +PythonDomain.object_types["data"].roles = ("data", "class", "obj") + +intersphinx_mapping = {"python": ("https://docs.python.org/3", None)} + +spelling_show_suggestions = True + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + +# Configure viewcode extension. +from websockets.version import commit + +code_url = f"https://github.com/python-websockets/websockets/blob/{commit}" + +def linkcode_resolve(domain, info): + # Non-linkable objects from the starter kit in the tutorial. + if domain == "js" or info["module"] == "connect4": + return + + assert domain == "py", "expected only Python objects" + + mod = importlib.import_module(info["module"]) + if "." in info["fullname"]: + objname, attrname = info["fullname"].split(".") + obj = getattr(mod, objname) + try: + # object is a method of a class + obj = getattr(obj, attrname) + except AttributeError: + # object is an attribute of a class + return None + else: + obj = getattr(mod, info["fullname"]) + + try: + file = inspect.getsourcefile(obj) + lines = inspect.getsourcelines(obj) + except TypeError: + # e.g. object is a typing.Union + return None + file = os.path.relpath(file, os.path.abspath("..")) + if not file.startswith("src/websockets"): + # e.g. object is a typing.NewType + return None + start, end = lines[1], lines[1] + len(lines[0]) - 1 + + return f"{code_url}/{file}#L{start}-L{end}" + +# Configure opengraph extension + +# Social cards don't support the SVG logo. Also, the text preview looks bad. +ogp_social_cards = {"enable": False} + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = "furo" + +html_theme_options = { + "light_css_variables": { + "color-brand-primary": "#306998", # blue from logo + "color-brand-content": "#0b487a", # blue more saturated and less dark + }, + "dark_css_variables": { + "color-brand-primary": "#ffd43bcc", # yellow from logo, more muted than content + "color-brand-content": "#ffd43bd9", # yellow from logo, transparent like text + }, + "sidebar_hide_name": True, +} + +html_logo = "_static/websockets.svg" + +html_favicon = "_static/favicon.ico" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] + +html_copy_source = False + +html_show_sphinx = False diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/faq/asyncio.rst b/tests/wpt/tests/tools/third_party/websockets/docs/faq/asyncio.rst new file mode 100644 index 00000000000..e77f50adddc --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/faq/asyncio.rst @@ -0,0 +1,69 @@ +Using asyncio +============= + +.. currentmodule:: websockets + +How do I run two coroutines in parallel? +---------------------------------------- + +You must start two tasks, which the event loop will run concurrently. You can +achieve this with :func:`asyncio.gather` or :func:`asyncio.create_task`. + +Keep track of the tasks and make sure they terminate or you cancel them when +the connection terminates. + +Why does my program never receive any messages? +----------------------------------------------- + +Your program runs a coroutine that never yields control to the event loop. The +coroutine that receives messages never gets a chance to run. + +Putting an ``await`` statement in a ``for`` or a ``while`` loop isn't enough +to yield control. Awaiting a coroutine may yield control, but there's no +guarantee that it will. + +For example, :meth:`~legacy.protocol.WebSocketCommonProtocol.send` only yields +control when send buffers are full, which never happens in most practical +cases. + +If you run a loop that contains only synchronous operations and +a :meth:`~legacy.protocol.WebSocketCommonProtocol.send` call, you must yield +control explicitly with :func:`asyncio.sleep`:: + + async def producer(websocket): + message = generate_next_message() + await websocket.send(message) + await asyncio.sleep(0) # yield control to the event loop + +:func:`asyncio.sleep` always suspends the current task, allowing other tasks +to run. This behavior is documented precisely because it isn't expected from +every coroutine. + +See `issue 867`_. + +.. _issue 867: https://github.com/python-websockets/websockets/issues/867 + +Why am I having problems with threads? +-------------------------------------- + +If you choose websockets' default implementation based on :mod:`asyncio`, then +you shouldn't use threads. Indeed, choosing :mod:`asyncio` to handle concurrency +is mutually exclusive with :mod:`threading`. + +If you believe that you need to run websockets in a thread and some logic in +another thread, you should run that logic in a :class:`~asyncio.Task` instead. +If it blocks the event loop, :meth:`~asyncio.loop.run_in_executor` will help. + +This question is really about :mod:`asyncio`. Please review the advice about +:ref:`asyncio-multithreading` in the Python documentation. + +Why does my simple program misbehave mysteriously? +-------------------------------------------------- + +You are using :func:`time.sleep` instead of :func:`asyncio.sleep`, which +blocks the event loop and prevents asyncio from operating normally. + +This may lead to messages getting send but not received, to connection +timeouts, and to unexpected results of shotgun debugging e.g. adding an +unnecessary call to :meth:`~legacy.protocol.WebSocketCommonProtocol.send` +makes the program functional. diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/faq/client.rst b/tests/wpt/tests/tools/third_party/websockets/docs/faq/client.rst new file mode 100644 index 00000000000..c590ac107db --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/faq/client.rst @@ -0,0 +1,101 @@ +Client +====== + +.. currentmodule:: websockets + +Why does the client close the connection prematurely? +----------------------------------------------------- + +You're exiting the context manager prematurely. Wait for the work to be +finished before exiting. + +For example, if your code has a structure similar to:: + + async with connect(...) as websocket: + asyncio.create_task(do_some_work()) + +change it to:: + + async with connect(...) as websocket: + await do_some_work() + +How do I access HTTP headers? +----------------------------- + +Once the connection is established, HTTP headers are available in +:attr:`~client.WebSocketClientProtocol.request_headers` and +:attr:`~client.WebSocketClientProtocol.response_headers`. + +How do I set HTTP headers? +-------------------------- + +To set the ``Origin``, ``Sec-WebSocket-Extensions``, or +``Sec-WebSocket-Protocol`` headers in the WebSocket handshake request, use the +``origin``, ``extensions``, or ``subprotocols`` arguments of +:func:`~client.connect`. + +To override the ``User-Agent`` header, use the ``user_agent_header`` argument. +Set it to :obj:`None` to remove the header. + +To set other HTTP headers, for example the ``Authorization`` header, use the +``extra_headers`` argument:: + + async with connect(..., extra_headers={"Authorization": ...}) as websocket: + ... + +In the :mod:`threading` API, this argument is named ``additional_headers``:: + + with connect(..., additional_headers={"Authorization": ...}) as websocket: + ... + +How do I force the IP address that the client connects to? +---------------------------------------------------------- + +Use the ``host`` argument of :meth:`~asyncio.loop.create_connection`:: + + await websockets.connect("ws://example.com", host="192.168.0.1") + +:func:`~client.connect` accepts the same arguments as +:meth:`~asyncio.loop.create_connection`. + +How do I close a connection? +---------------------------- + +The easiest is to use :func:`~client.connect` as a context manager:: + + async with connect(...) as websocket: + ... + +The connection is closed when exiting the context manager. + +How do I reconnect when the connection drops? +--------------------------------------------- + +Use :func:`~client.connect` as an asynchronous iterator:: + + async for websocket in websockets.connect(...): + try: + ... + except websockets.ConnectionClosed: + continue + +Make sure you handle exceptions in the ``async for`` loop. Uncaught exceptions +will break out of the loop. + +How do I stop a client that is processing messages in a loop? +------------------------------------------------------------- + +You can close the connection. + +Here's an example that terminates cleanly when it receives SIGTERM on Unix: + +.. literalinclude:: ../../example/faq/shutdown_client.py + :emphasize-lines: 10-13 + +How do I disable TLS/SSL certificate verification? +-------------------------------------------------- + +Look at the ``ssl`` argument of :meth:`~asyncio.loop.create_connection`. + +:func:`~client.connect` accepts the same arguments as +:meth:`~asyncio.loop.create_connection`. diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/faq/common.rst b/tests/wpt/tests/tools/third_party/websockets/docs/faq/common.rst new file mode 100644 index 00000000000..2c63c4f36f1 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/faq/common.rst @@ -0,0 +1,161 @@ +Both sides +========== + +.. currentmodule:: websockets + +What does ``ConnectionClosedError: no close frame received or sent`` mean? +-------------------------------------------------------------------------- + +If you're seeing this traceback in the logs of a server: + +.. code-block:: pytb + + connection handler failed + Traceback (most recent call last): + ... + asyncio.exceptions.IncompleteReadError: 0 bytes read on a total of 2 expected bytes + + The above exception was the direct cause of the following exception: + + Traceback (most recent call last): + ... + websockets.exceptions.ConnectionClosedError: no close frame received or sent + +or if a client crashes with this traceback: + +.. code-block:: pytb + + Traceback (most recent call last): + ... + ConnectionResetError: [Errno 54] Connection reset by peer + + The above exception was the direct cause of the following exception: + + Traceback (most recent call last): + ... + websockets.exceptions.ConnectionClosedError: no close frame received or sent + +it means that the TCP connection was lost. As a consequence, the WebSocket +connection was closed without receiving and sending a close frame, which is +abnormal. + +You can catch and handle :exc:`~exceptions.ConnectionClosed` to prevent it +from being logged. + +There are several reasons why long-lived connections may be lost: + +* End-user devices tend to lose network connectivity often and unpredictably + because they can move out of wireless network coverage, get unplugged from + a wired network, enter airplane mode, be put to sleep, etc. +* HTTP load balancers or proxies that aren't configured for long-lived + connections may terminate connections after a short amount of time, usually + 30 seconds, despite websockets' keepalive mechanism. + +If you're facing a reproducible issue, :ref:`enable debug logs <debugging>` to +see when and how connections are closed. + +What does ``ConnectionClosedError: sent 1011 (internal error) keepalive ping timeout; no close frame received`` mean? +--------------------------------------------------------------------------------------------------------------------- + +If you're seeing this traceback in the logs of a server: + +.. code-block:: pytb + + connection handler failed + Traceback (most recent call last): + ... + asyncio.exceptions.CancelledError + + The above exception was the direct cause of the following exception: + + Traceback (most recent call last): + ... + websockets.exceptions.ConnectionClosedError: sent 1011 (internal error) keepalive ping timeout; no close frame received + +or if a client crashes with this traceback: + +.. code-block:: pytb + + Traceback (most recent call last): + ... + asyncio.exceptions.CancelledError + + The above exception was the direct cause of the following exception: + + Traceback (most recent call last): + ... + websockets.exceptions.ConnectionClosedError: sent 1011 (internal error) keepalive ping timeout; no close frame received + +it means that the WebSocket connection suffered from excessive latency and was +closed after reaching the timeout of websockets' keepalive mechanism. + +You can catch and handle :exc:`~exceptions.ConnectionClosed` to prevent it +from being logged. + +There are two main reasons why latency may increase: + +* Poor network connectivity. +* More traffic than the recipient can handle. + +See the discussion of :doc:`timeouts <../topics/timeouts>` for details. + +If websockets' default timeout of 20 seconds is too short for your use case, +you can adjust it with the ``ping_timeout`` argument. + +How do I set a timeout on :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`? +-------------------------------------------------------------------------------- + +On Python ≥ 3.11, use :func:`asyncio.timeout`:: + + async with asyncio.timeout(timeout=10): + message = await websocket.recv() + +On older versions of Python, use :func:`asyncio.wait_for`:: + + message = await asyncio.wait_for(websocket.recv(), timeout=10) + +This technique works for most APIs. When it doesn't, for example with +asynchronous context managers, websockets provides an ``open_timeout`` argument. + +How can I pass arguments to a custom protocol subclass? +------------------------------------------------------- + +You can bind additional arguments to the protocol factory with +:func:`functools.partial`:: + + import asyncio + import functools + import websockets + + class MyServerProtocol(websockets.WebSocketServerProtocol): + def __init__(self, *args, extra_argument=None, **kwargs): + super().__init__(*args, **kwargs) + # do something with extra_argument + + create_protocol = functools.partial(MyServerProtocol, extra_argument=42) + start_server = websockets.serve(..., create_protocol=create_protocol) + +This example was for a server. The same pattern applies on a client. + +How do I keep idle connections open? +------------------------------------ + +websockets sends pings at 20 seconds intervals to keep the connection open. + +It closes the connection if it doesn't get a pong within 20 seconds. + +You can adjust this behavior with ``ping_interval`` and ``ping_timeout``. + +See :doc:`../topics/timeouts` for details. + +How do I respond to pings? +-------------------------- + +If you are referring to Ping_ and Pong_ frames defined in the WebSocket +protocol, don't bother, because websockets handles them for you. + +.. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2 +.. _Pong: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.3 + +If you are connecting to a server that defines its own heartbeat at the +application level, then you need to build that logic into your application. diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/faq/index.rst b/tests/wpt/tests/tools/third_party/websockets/docs/faq/index.rst new file mode 100644 index 00000000000..9d5b0d538ac --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/faq/index.rst @@ -0,0 +1,21 @@ +Frequently asked questions +========================== + +.. currentmodule:: websockets + +.. admonition:: Many questions asked in websockets' issue tracker are really + about :mod:`asyncio`. + :class: seealso + + Python's documentation about `developing with asyncio`_ is a good + complement. + + .. _developing with asyncio: https://docs.python.org/3/library/asyncio-dev.html + +.. toctree:: + + server + client + common + asyncio + misc diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/faq/misc.rst b/tests/wpt/tests/tools/third_party/websockets/docs/faq/misc.rst new file mode 100644 index 00000000000..ee5ad23728e --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/faq/misc.rst @@ -0,0 +1,49 @@ +Miscellaneous +============= + +.. currentmodule:: websockets + +Why do I get the error: ``module 'websockets' has no attribute '...'``? +....................................................................... + +Often, this is because you created a script called ``websockets.py`` in your +current working directory. Then ``import websockets`` imports this module +instead of the websockets library. + +.. _real-import-paths: + +Why is the default implementation located in ``websockets.legacy``? +................................................................... + +This is an artifact of websockets' history. For its first eight years, only the +:mod:`asyncio` implementation existed. Then, the Sans-I/O implementation was +added. Moving the code in a ``legacy`` submodule eased this refactoring and +optimized maintainability. + +All public APIs were kept at their original locations. ``websockets.legacy`` +isn't a public API. It's only visible in the source code and in stack traces. +There is no intent to deprecate this implementation — at least until a superior +alternative exists. + +Why is websockets slower than another library in my benchmark? +.............................................................. + +Not all libraries are as feature-complete as websockets. For a fair benchmark, +you should disable features that the other library doesn't provide. Typically, +you may need to disable: + +* Compression: set ``compression=None`` +* Keepalive: set ``ping_interval=None`` +* UTF-8 decoding: send ``bytes`` rather than ``str`` + +If websockets is still slower than another Python library, please file a bug. + +Are there ``onopen``, ``onmessage``, ``onerror``, and ``onclose`` callbacks? +............................................................................ + +No, there aren't. + +websockets provides high-level, coroutine-based APIs. Compared to callbacks, +coroutines make it easier to manage control flow in concurrent code. + +If you prefer callback-based APIs, you should use another library. diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/faq/server.rst b/tests/wpt/tests/tools/third_party/websockets/docs/faq/server.rst new file mode 100644 index 00000000000..08b412d306b --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/faq/server.rst @@ -0,0 +1,336 @@ +Server +====== + +.. currentmodule:: websockets + +Why does the server close the connection prematurely? +----------------------------------------------------- + +Your connection handler exits prematurely. Wait for the work to be finished +before returning. + +For example, if your handler has a structure similar to:: + + async def handler(websocket): + asyncio.create_task(do_some_work()) + +change it to:: + + async def handler(websocket): + await do_some_work() + +Why does the server close the connection after one message? +----------------------------------------------------------- + +Your connection handler exits after processing one message. Write a loop to +process multiple messages. + +For example, if your handler looks like this:: + + async def handler(websocket): + print(websocket.recv()) + +change it like this:: + + async def handler(websocket): + async for message in websocket: + print(message) + +*Don't feel bad if this happens to you — it's the most common question in +websockets' issue tracker :-)* + +Why can only one client connect at a time? +------------------------------------------ + +Your connection handler blocks the event loop. Look for blocking calls. + +Any call that may take some time must be asynchronous. + +For example, this connection handler prevents the event loop from running during +one second:: + + async def handler(websocket): + time.sleep(1) + ... + +Change it to:: + + async def handler(websocket): + await asyncio.sleep(1) + ... + +In addition, calling a coroutine doesn't guarantee that it will yield control to +the event loop. + +For example, this connection handler blocks the event loop by sending messages +continuously:: + + async def handler(websocket): + while True: + await websocket.send("firehose!") + +:meth:`~legacy.protocol.WebSocketCommonProtocol.send` completes synchronously as +long as there's space in send buffers. The event loop never runs. (This pattern +is uncommon in real-world applications. It occurs mostly in toy programs.) + +You can avoid the issue by yielding control to the event loop explicitly:: + + async def handler(websocket): + while True: + await websocket.send("firehose!") + await asyncio.sleep(0) + +All this is part of learning asyncio. It isn't specific to websockets. + +See also Python's documentation about `running blocking code`_. + +.. _running blocking code: https://docs.python.org/3/library/asyncio-dev.html#running-blocking-code + +.. _send-message-to-all-users: + +How do I send a message to all users? +------------------------------------- + +Record all connections in a global variable:: + + CONNECTIONS = set() + + async def handler(websocket): + CONNECTIONS.add(websocket) + try: + await websocket.wait_closed() + finally: + CONNECTIONS.remove(websocket) + +Then, call :func:`~websockets.broadcast`:: + + import websockets + + def message_all(message): + websockets.broadcast(CONNECTIONS, message) + +If you're running multiple server processes, make sure you call ``message_all`` +in each process. + +.. _send-message-to-single-user: + +How do I send a message to a single user? +----------------------------------------- + +Record connections in a global variable, keyed by user identifier:: + + CONNECTIONS = {} + + async def handler(websocket): + user_id = ... # identify user in your app's context + CONNECTIONS[user_id] = websocket + try: + await websocket.wait_closed() + finally: + del CONNECTIONS[user_id] + +Then, call :meth:`~legacy.protocol.WebSocketCommonProtocol.send`:: + + async def message_user(user_id, message): + websocket = CONNECTIONS[user_id] # raises KeyError if user disconnected + await websocket.send(message) # may raise websockets.ConnectionClosed + +Add error handling according to the behavior you want if the user disconnected +before the message could be sent. + +This example supports only one connection per user. To support concurrent +connections by the same user, you can change ``CONNECTIONS`` to store a set of +connections for each user. + +If you're running multiple server processes, call ``message_user`` in each +process. The process managing the user's connection sends the message; other +processes do nothing. + +When you reach a scale where server processes cannot keep up with the stream of +all messages, you need a better architecture. For example, you could deploy an +external publish / subscribe system such as Redis_. Server processes would +subscribe their clients. Then, they would receive messages only for the +connections that they're managing. + +.. _Redis: https://redis.io/ + +How do I send a message to a channel, a topic, or some users? +------------------------------------------------------------- + +websockets doesn't provide built-in publish / subscribe functionality. + +Record connections in a global variable, keyed by user identifier, as shown in +:ref:`How do I send a message to a single user?<send-message-to-single-user>` + +Then, build the set of recipients and broadcast the message to them, as shown in +:ref:`How do I send a message to all users?<send-message-to-all-users>` + +:doc:`../howto/django` contains a complete implementation of this pattern. + +Again, as you scale, you may reach the performance limits of a basic in-process +implementation. You may need an external publish / subscribe system like Redis_. + +.. _Redis: https://redis.io/ + +How do I pass arguments to the connection handler? +-------------------------------------------------- + +You can bind additional arguments to the connection handler with +:func:`functools.partial`:: + + import asyncio + import functools + import websockets + + async def handler(websocket, extra_argument): + ... + + bound_handler = functools.partial(handler, extra_argument=42) + start_server = websockets.serve(bound_handler, ...) + +Another way to achieve this result is to define the ``handler`` coroutine in +a scope where the ``extra_argument`` variable exists instead of injecting it +through an argument. + +How do I access the request path? +--------------------------------- + +It is available in the :attr:`~server.WebSocketServerProtocol.path` attribute. + +You may route a connection to different handlers depending on the request path:: + + async def handler(websocket): + if websocket.path == "/blue": + await blue_handler(websocket) + elif websocket.path == "/green": + await green_handler(websocket) + else: + # No handler for this path; close the connection. + return + +You may also route the connection based on the first message received from the +client, as shown in the :doc:`tutorial <../intro/tutorial2>`. When you want to +authenticate the connection before routing it, this is usually more convenient. + +Generally speaking, there is far less emphasis on the request path in WebSocket +servers than in HTTP servers. When a WebSocket server provides a single endpoint, +it may ignore the request path entirely. + +How do I access HTTP headers? +----------------------------- + +To access HTTP headers during the WebSocket handshake, you can override +:attr:`~server.WebSocketServerProtocol.process_request`:: + + async def process_request(self, path, request_headers): + authorization = request_headers["Authorization"] + +Once the connection is established, HTTP headers are available in +:attr:`~server.WebSocketServerProtocol.request_headers` and +:attr:`~server.WebSocketServerProtocol.response_headers`:: + + async def handler(websocket): + authorization = websocket.request_headers["Authorization"] + +How do I set HTTP headers? +-------------------------- + +To set the ``Sec-WebSocket-Extensions`` or ``Sec-WebSocket-Protocol`` headers in +the WebSocket handshake response, use the ``extensions`` or ``subprotocols`` +arguments of :func:`~server.serve`. + +To override the ``Server`` header, use the ``server_header`` argument. Set it to +:obj:`None` to remove the header. + +To set other HTTP headers, use the ``extra_headers`` argument. + +How do I get the IP address of the client? +------------------------------------------ + +It's available in :attr:`~legacy.protocol.WebSocketCommonProtocol.remote_address`:: + + async def handler(websocket): + remote_ip = websocket.remote_address[0] + +How do I set the IP addresses that my server listens on? +-------------------------------------------------------- + +Use the ``host`` argument of :meth:`~asyncio.loop.create_server`:: + + await websockets.serve(handler, host="192.168.0.1", port=8080) + +:func:`~server.serve` accepts the same arguments as +:meth:`~asyncio.loop.create_server`. + +What does ``OSError: [Errno 99] error while attempting to bind on address ('::1', 80, 0, 0): address not available`` mean? +-------------------------------------------------------------------------------------------------------------------------- + +You are calling :func:`~server.serve` without a ``host`` argument in a context +where IPv6 isn't available. + +To listen only on IPv4, specify ``host="0.0.0.0"`` or ``family=socket.AF_INET``. + +Refer to the documentation of :meth:`~asyncio.loop.create_server` for details. + +How do I close a connection? +---------------------------- + +websockets takes care of closing the connection when the handler exits. + +How do I stop a server? +----------------------- + +Exit the :func:`~server.serve` context manager. + +Here's an example that terminates cleanly when it receives SIGTERM on Unix: + +.. literalinclude:: ../../example/faq/shutdown_server.py + :emphasize-lines: 12-15,18 + +How do I stop a server while keeping existing connections open? +--------------------------------------------------------------- + +Call the server's :meth:`~server.WebSocketServer.close` method with +``close_connections=False``. + +Here's how to adapt the example just above:: + + async def server(): + ... + + server = await websockets.serve(echo, "localhost", 8765) + await stop + await server.close(close_connections=False) + +How do I implement a health check? +---------------------------------- + +Intercept WebSocket handshake requests with the +:meth:`~server.WebSocketServerProtocol.process_request` hook. + +When a request is sent to the health check endpoint, treat is as an HTTP request +and return a ``(status, headers, body)`` tuple, as in this example: + +.. literalinclude:: ../../example/faq/health_check_server.py + :emphasize-lines: 7-9,18 + +How do I run HTTP and WebSocket servers on the same port? +--------------------------------------------------------- + +You don't. + +HTTP and WebSocket have widely different operational characteristics. Running +them with the same server becomes inconvenient when you scale. + +Providing an HTTP server is out of scope for websockets. It only aims at +providing a WebSocket server. + +There's limited support for returning HTTP responses with the +:attr:`~server.WebSocketServerProtocol.process_request` hook. + +If you need more, pick an HTTP server and run it separately. + +Alternatively, pick an HTTP framework that builds on top of ``websockets`` to +support WebSocket connections, like Sanic_. + +.. _Sanic: https://sanicframework.org/en/ diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/howto/autoreload.rst b/tests/wpt/tests/tools/third_party/websockets/docs/howto/autoreload.rst new file mode 100644 index 00000000000..fc736a59186 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/howto/autoreload.rst @@ -0,0 +1,31 @@ +Reload on code changes +====================== + +When developing a websockets server, you may run it locally to test changes. +Unfortunately, whenever you want to try a new version of the code, you must +stop the server and restart it, which slows down your development process. + +Web frameworks such as Django or Flask provide a development server that +reloads the application automatically when you make code changes. There is no +such functionality in websockets because it's designed for production rather +than development. + +However, you can achieve the same result easily. + +Install watchdog_ with the ``watchmedo`` shell utility: + +.. code-block:: console + + $ pip install 'watchdog[watchmedo]' + +.. _watchdog: https://pypi.org/project/watchdog/ + +Run your server with ``watchmedo auto-restart``: + +.. code-block:: console + + $ watchmedo auto-restart --pattern "*.py" --recursive --signal SIGTERM \ + python app.py + +This example assumes that the server is defined in a script called ``app.py``. +Adapt it as necessary. diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/howto/cheatsheet.rst b/tests/wpt/tests/tools/third_party/websockets/docs/howto/cheatsheet.rst new file mode 100644 index 00000000000..95b551f6731 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/howto/cheatsheet.rst @@ -0,0 +1,87 @@ +Cheat sheet +=========== + +.. currentmodule:: websockets + +Server +------ + +* Write a coroutine that handles a single connection. It receives a WebSocket + protocol instance and the URI path in argument. + + * Call :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` and + :meth:`~legacy.protocol.WebSocketCommonProtocol.send` to receive and send + messages at any time. + + * When :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` or + :meth:`~legacy.protocol.WebSocketCommonProtocol.send` raises + :exc:`~exceptions.ConnectionClosed`, clean up and exit. If you started + other :class:`asyncio.Task`, terminate them before exiting. + + * If you aren't awaiting :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`, + consider awaiting :meth:`~legacy.protocol.WebSocketCommonProtocol.wait_closed` + to detect quickly when the connection is closed. + + * You may :meth:`~legacy.protocol.WebSocketCommonProtocol.ping` or + :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` if you wish but it isn't + needed in general. + +* Create a server with :func:`~server.serve` which is similar to asyncio's + :meth:`~asyncio.loop.create_server`. You can also use it as an asynchronous + context manager. + + * The server takes care of establishing connections, then lets the handler + execute the application logic, and finally closes the connection after the + handler exits normally or with an exception. + + * For advanced customization, you may subclass + :class:`~server.WebSocketServerProtocol` and pass either this subclass or + a factory function as the ``create_protocol`` argument. + +Client +------ + +* Create a client with :func:`~client.connect` which is similar to asyncio's + :meth:`~asyncio.loop.create_connection`. You can also use it as an + asynchronous context manager. + + * For advanced customization, you may subclass + :class:`~client.WebSocketClientProtocol` and pass either this subclass or + a factory function as the ``create_protocol`` argument. + +* Call :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` and + :meth:`~legacy.protocol.WebSocketCommonProtocol.send` to receive and send messages + at any time. + +* You may :meth:`~legacy.protocol.WebSocketCommonProtocol.ping` or + :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` if you wish but it isn't + needed in general. + +* If you aren't using :func:`~client.connect` as a context manager, call + :meth:`~legacy.protocol.WebSocketCommonProtocol.close` to terminate the connection. + +.. _debugging: + +Debugging +--------- + +If you don't understand what websockets is doing, enable logging:: + + import logging + logger = logging.getLogger('websockets') + logger.setLevel(logging.DEBUG) + logger.addHandler(logging.StreamHandler()) + +The logs contain: + +* Exceptions in the connection handler at the ``ERROR`` level +* Exceptions in the opening or closing handshake at the ``INFO`` level +* All frames at the ``DEBUG`` level — this can be very verbose + +If you're new to ``asyncio``, you will certainly encounter issues that are +related to asynchronous programming in general rather than to websockets in +particular. Fortunately Python's official documentation provides advice to +`develop with asyncio`_. Check it out: it's invaluable! + +.. _develop with asyncio: https://docs.python.org/3/library/asyncio-dev.html + diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/howto/django.rst b/tests/wpt/tests/tools/third_party/websockets/docs/howto/django.rst new file mode 100644 index 00000000000..e3da0a878b8 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/howto/django.rst @@ -0,0 +1,294 @@ +Integrate with Django +===================== + +If you're looking at adding real-time capabilities to a Django project with +WebSocket, you have two main options. + +1. Using Django Channels_, a project adding WebSocket to Django, among other + features. This approach is fully supported by Django. However, it requires + switching to a new deployment architecture. + +2. Deploying a separate WebSocket server next to your Django project. This + technique is well suited when you need to add a small set of real-time + features — maybe a notification service — to an HTTP application. + +.. _Channels: https://channels.readthedocs.io/ + +This guide shows how to implement the second technique with websockets. It +assumes familiarity with Django. + +Authenticate connections +------------------------ + +Since the websockets server runs outside of Django, we need to integrate it +with ``django.contrib.auth``. + +We will generate authentication tokens in the Django project. Then we will +send them to the websockets server, where they will authenticate the user. + +Generating a token for the current user and making it available in the browser +is up to you. You could render the token in a template or fetch it with an API +call. + +Refer to the topic guide on :doc:`authentication <../topics/authentication>` +for details on this design. + +Generate tokens +............... + +We want secure, short-lived tokens containing the user ID. We'll rely on +`django-sesame`_, a small library designed exactly for this purpose. + +.. _django-sesame: https://github.com/aaugustin/django-sesame + +Add django-sesame to the dependencies of your Django project, install it, and +configure it in the settings of the project: + +.. code-block:: python + + AUTHENTICATION_BACKENDS = [ + "django.contrib.auth.backends.ModelBackend", + "sesame.backends.ModelBackend", + ] + +(If your project already uses another authentication backend than the default +``"django.contrib.auth.backends.ModelBackend"``, adjust accordingly.) + +You don't need ``"sesame.middleware.AuthenticationMiddleware"``. It is for +authenticating users in the Django server, while we're authenticating them in +the websockets server. + +We'd like our tokens to be valid for 30 seconds. We expect web pages to load +and to establish the WebSocket connection within this delay. Configure +django-sesame accordingly in the settings of your Django project: + +.. code-block:: python + + SESAME_MAX_AGE = 30 + +If you expect your web site to load faster for all clients, a shorter lifespan +is possible. However, in the context of this document, it would make manual +testing more difficult. + +You could also enable single-use tokens. However, this would update the last +login date of the user every time a WebSocket connection is established. This +doesn't seem like a good idea, both in terms of behavior and in terms of +performance. + +Now you can generate tokens in a ``django-admin shell`` as follows: + +.. code-block:: pycon + + >>> from django.contrib.auth import get_user_model + >>> User = get_user_model() + >>> user = User.objects.get(username="<your username>") + >>> from sesame.utils import get_token + >>> get_token(user) + '<your token>' + +Keep this console open: since tokens expire after 30 seconds, you'll have to +generate a new token every time you want to test connecting to the server. + +Validate tokens +............... + +Let's move on to the websockets server. + +Add websockets to the dependencies of your Django project and install it. +Indeed, we're going to reuse the environment of the Django project, so we can +call its APIs in the websockets server. + +Now here's how to implement authentication. + +.. literalinclude:: ../../example/django/authentication.py + +Let's unpack this code. + +We're calling ``django.setup()`` before doing anything with Django because +we're using Django in a `standalone script`_. This assumes that the +``DJANGO_SETTINGS_MODULE`` environment variable is set to the Python path to +your settings module. + +.. _standalone script: https://docs.djangoproject.com/en/stable/topics/settings/#calling-django-setup-is-required-for-standalone-django-usage + +The connection handler reads the first message received from the client, which +is expected to contain a django-sesame token. Then it authenticates the user +with ``get_user()``, the API for `authentication outside a view`_. If +authentication fails, it closes the connection and exits. + +.. _authentication outside a view: https://django-sesame.readthedocs.io/en/stable/howto.html#outside-a-view + +When we call an API that makes a database query such as ``get_user()``, we +wrap the call in :func:`~asyncio.to_thread`. Indeed, the Django ORM doesn't +support asynchronous I/O. It would block the event loop if it didn't run in a +separate thread. :func:`~asyncio.to_thread` is available since Python 3.9. In +earlier versions, use :meth:`~asyncio.loop.run_in_executor` instead. + +Finally, we start a server with :func:`~websockets.server.serve`. + +We're ready to test! + +Save this code to a file called ``authentication.py``, make sure the +``DJANGO_SETTINGS_MODULE`` environment variable is set properly, and start the +websockets server: + +.. code-block:: console + + $ python authentication.py + +Generate a new token — remember, they're only valid for 30 seconds — and use +it to connect to your server. Paste your token and press Enter when you get a +prompt: + +.. code-block:: console + + $ python -m websockets ws://localhost:8888/ + Connected to ws://localhost:8888/ + > <your token> + < Hello <your username>! + Connection closed: 1000 (OK). + +It works! + +If you enter an expired or invalid token, authentication fails and the server +closes the connection: + +.. code-block:: console + + $ python -m websockets ws://localhost:8888/ + Connected to ws://localhost:8888. + > not a token + Connection closed: 1011 (internal error) authentication failed. + +You can also test from a browser by generating a new token and running the +following code in the JavaScript console of the browser: + +.. code-block:: javascript + + websocket = new WebSocket("ws://localhost:8888/"); + websocket.onopen = (event) => websocket.send("<your token>"); + websocket.onmessage = (event) => console.log(event.data); + +If you don't want to import your entire Django project into the websockets +server, you can build a separate Django project with ``django.contrib.auth``, +``django-sesame``, a suitable ``User`` model, and a subset of the settings of +the main project. + +Stream events +------------- + +We can connect and authenticate but our server doesn't do anything useful yet! + +Let's send a message every time a user makes an action in the admin. This +message will be broadcast to all users who can access the model on which the +action was made. This may be used for showing notifications to other users. + +Many use cases for WebSocket with Django follow a similar pattern. + +Set up event bus +................ + +We need a event bus to enable communications between Django and websockets. +Both sides connect permanently to the bus. Then Django writes events and +websockets reads them. For the sake of simplicity, we'll rely on `Redis +Pub/Sub`_. + +.. _Redis Pub/Sub: https://redis.io/topics/pubsub + +The easiest way to add Redis to a Django project is by configuring a cache +backend with `django-redis`_. This library manages connections to Redis +efficiently, persisting them between requests, and provides an API to access +the Redis connection directly. + +.. _django-redis: https://github.com/jazzband/django-redis + +Install Redis, add django-redis to the dependencies of your Django project, +install it, and configure it in the settings of the project: + +.. code-block:: python + + CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": "redis://127.0.0.1:6379/1", + }, + } + +If you already have a default cache, add a new one with a different name and +change ``get_redis_connection("default")`` in the code below to the same name. + +Publish events +.............. + +Now let's write events to the bus. + +Add the following code to a module that is imported when your Django project +starts. Typically, you would put it in a ``signals.py`` module, which you +would import in the ``AppConfig.ready()`` method of one of your apps: + +.. literalinclude:: ../../example/django/signals.py + +This code runs every time the admin saves a ``LogEntry`` object to keep track +of a change. It extracts interesting data, serializes it to JSON, and writes +an event to Redis. + +Let's check that it works: + +.. code-block:: console + + $ redis-cli + 127.0.0.1:6379> SELECT 1 + OK + 127.0.0.1:6379[1]> SUBSCRIBE events + Reading messages... (press Ctrl-C to quit) + 1) "subscribe" + 2) "events" + 3) (integer) 1 + +Leave this command running, start the Django development server and make +changes in the admin: add, modify, or delete objects. You should see +corresponding events published to the ``"events"`` stream. + +Broadcast events +................ + +Now let's turn to reading events and broadcasting them to connected clients. +We need to add several features: + +* Keep track of connected clients so we can broadcast messages. +* Tell which content types the user has permission to view or to change. +* Connect to the message bus and read events. +* Broadcast these events to users who have corresponding permissions. + +Here's a complete implementation. + +.. literalinclude:: ../../example/django/notifications.py + +Since the ``get_content_types()`` function makes a database query, it is +wrapped inside :func:`asyncio.to_thread()`. It runs once when each WebSocket +connection is open; then its result is cached for the lifetime of the +connection. Indeed, running it for each message would trigger database queries +for all connected users at the same time, which would hurt the database. + +The connection handler merely registers the connection in a global variable, +associated to the list of content types for which events should be sent to +that connection, and waits until the client disconnects. + +The ``process_events()`` function reads events from Redis and broadcasts them +to all connections that should receive them. We don't care much if a sending a +notification fails — this happens when a connection drops between the moment +we iterate on connections and the moment the corresponding message is sent — +so we start a task with for each message and forget about it. Also, this means +we're immediately ready to process the next event, even if it takes time to +send a message to a slow client. + +Since Redis can publish a message to multiple subscribers, multiple instances +of this server can safely run in parallel. + +Does it scale? +-------------- + +In theory, given enough servers, this design can scale to a hundred million +clients, since Redis can handle ten thousand servers and each server can +handle ten thousand clients. In practice, you would need a more scalable +message bus before reaching that scale, due to the volume of messages. diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/howto/extensions.rst b/tests/wpt/tests/tools/third_party/websockets/docs/howto/extensions.rst new file mode 100644 index 00000000000..3c8a7d72a64 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/howto/extensions.rst @@ -0,0 +1,30 @@ +Write an extension +================== + +.. currentmodule:: websockets.extensions + +During the opening handshake, WebSocket clients and servers negotiate which +extensions_ will be used with which parameters. Then each frame is processed +by extensions before being sent or after being received. + +.. _extensions: https://www.rfc-editor.org/rfc/rfc6455.html#section-9 + +As a consequence, writing an extension requires implementing several classes: + +* Extension Factory: it negotiates parameters and instantiates the extension. + + Clients and servers require separate extension factories with distinct APIs. + + Extension factories are the public API of an extension. + +* Extension: it decodes incoming frames and encodes outgoing frames. + + If the extension is symmetrical, clients and servers can use the same + class. + + Extensions are initialized by extension factories, so they don't need to be + part of the public API of an extension. + +websockets provides base classes for extension factories and extensions. +See :class:`ClientExtensionFactory`, :class:`ServerExtensionFactory`, +and :class:`Extension` for details. diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/howto/fly.rst b/tests/wpt/tests/tools/third_party/websockets/docs/howto/fly.rst new file mode 100644 index 00000000000..ed001a2aeed --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/howto/fly.rst @@ -0,0 +1,177 @@ +Deploy to Fly +================ + +This guide describes how to deploy a websockets server to Fly_. + +.. _Fly: https://fly.io/ + +.. admonition:: The free tier of Fly is sufficient for trying this guide. + :class: tip + + The `free tier`__ include up to three small VMs. This guide uses only one. + + __ https://fly.io/docs/about/pricing/ + +We're going to deploy a very simple app. The process would be identical for a +more realistic app. + +Create application +------------------ + +Here's the implementation of the app, an echo server. Save it in a file called +``app.py``: + +.. literalinclude:: ../../example/deployment/fly/app.py + :language: python + +This app implements typical requirements for running on a Platform as a Service: + +* it provides a health check at ``/healthz``; +* it closes connections and exits cleanly when it receives a ``SIGTERM`` signal. + +Create a ``requirements.txt`` file containing this line to declare a dependency +on websockets: + +.. literalinclude:: ../../example/deployment/fly/requirements.txt + :language: text + +The app is ready. Let's deploy it! + +Deploy application +------------------ + +Follow the instructions__ to install the Fly CLI, if you haven't done that yet. + +__ https://fly.io/docs/hands-on/install-flyctl/ + +Sign up or log in to Fly. + +Launch the app — you'll have to pick a different name because I'm already using +``websockets-echo``: + +.. code-block:: console + + $ fly launch + Creating app in ... + Scanning source code + Detected a Python app + Using the following build configuration: + Builder: paketobuildpacks/builder:base + ? App Name (leave blank to use an auto-generated name): websockets-echo + ? Select organization: ... + ? Select region: ... + Created app websockets-echo in organization ... + Wrote config file fly.toml + ? Would you like to set up a Postgresql database now? No + We have generated a simple Procfile for you. Modify it to fit your needs and run "fly deploy" to deploy your application. + +.. admonition:: This will build the image with a generic buildpack. + :class: tip + + Fly can `build images`__ with a Dockerfile or a buildpack. Here, ``fly + launch`` configures a generic Paketo buildpack. + + If you'd rather package the app with a Dockerfile, check out the guide to + :ref:`containerize an application <containerize-application>`. + + __ https://fly.io/docs/reference/builders/ + +Replace the auto-generated ``fly.toml`` with: + +.. literalinclude:: ../../example/deployment/fly/fly.toml + :language: toml + +This configuration: + +* listens on port 443, terminates TLS, and forwards to the app on port 8080; +* declares a health check at ``/healthz``; +* requests a ``SIGTERM`` for terminating the app. + +Replace the auto-generated ``Procfile`` with: + +.. literalinclude:: ../../example/deployment/fly/Procfile + :language: text + +This tells Fly how to run the app. + +Now you can deploy it: + +.. code-block:: console + + $ fly deploy + + ... lots of output... + + ==> Monitoring deployment + + 1 desired, 1 placed, 1 healthy, 0 unhealthy [health checks: 1 total, 1 passing] + --> v0 deployed successfully + +Validate deployment +------------------- + +Let's confirm that your application is running as expected. + +Since it's a WebSocket server, you need a WebSocket client, such as the +interactive client that comes with websockets. + +If you're currently building a websockets server, perhaps you're already in a +virtualenv where websockets is installed. If not, you can install it in a new +virtualenv as follows: + +.. code-block:: console + + $ python -m venv websockets-client + $ . websockets-client/bin/activate + $ pip install websockets + +Connect the interactive client — you must replace ``websockets-echo`` with the +name of your Fly app in this command: + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.fly.dev/ + Connected to wss://websockets-echo.fly.dev/. + > + +Great! Your app is running! + +Once you're connected, you can send any message and the server will echo it, +or press Ctrl-D to terminate the connection: + +.. code-block:: console + + > Hello! + < Hello! + Connection closed: 1000 (OK). + +You can also confirm that your application shuts down gracefully. + +Connect an interactive client again — remember to replace ``websockets-echo`` +with your app: + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.fly.dev/ + Connected to wss://websockets-echo.fly.dev/. + > + +In another shell, restart the app — again, replace ``websockets-echo`` with your +app: + +.. code-block:: console + + $ fly restart websockets-echo + websockets-echo is being restarted + +Go back to the first shell. The connection is closed with code 1001 (going +away). + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.fly.dev/ + Connected to wss://websockets-echo.fly.dev/. + Connection closed: 1001 (going away). + +If graceful shutdown wasn't working, the server wouldn't perform a closing +handshake and the connection would be closed with code 1006 (abnormal closure). diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/howto/haproxy.rst b/tests/wpt/tests/tools/third_party/websockets/docs/howto/haproxy.rst new file mode 100644 index 00000000000..fdaab04011c --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/howto/haproxy.rst @@ -0,0 +1,61 @@ +Deploy behind HAProxy +===================== + +This guide demonstrates a way to load balance connections across multiple +websockets server processes running on the same machine with HAProxy_. + +We'll run server processes with Supervisor as described in :doc:`this guide +<supervisor>`. + +.. _HAProxy: https://www.haproxy.org/ + +Run server processes +-------------------- + +Save this app to ``app.py``: + +.. literalinclude:: ../../example/deployment/haproxy/app.py + :emphasize-lines: 24 + +Each server process listens on a different port by extracting an incremental +index from an environment variable set by Supervisor. + +Save this configuration to ``supervisord.conf``: + +.. literalinclude:: ../../example/deployment/haproxy/supervisord.conf + +This configuration runs four instances of the app. + +Install Supervisor and run it: + +.. code-block:: console + + $ supervisord -c supervisord.conf -n + +Configure and run HAProxy +------------------------- + +Here's a simple HAProxy configuration to load balance connections across four +processes: + +.. literalinclude:: ../../example/deployment/haproxy/haproxy.cfg + +In the backend configuration, we set the load balancing method to +``leastconn`` in order to balance the number of active connections across +servers. This is best for long running connections. + +Save the configuration to ``haproxy.cfg``, install HAProxy, and run it: + +.. code-block:: console + + $ haproxy -f haproxy.cfg + +You can confirm that HAProxy proxies connections properly: + +.. code-block:: console + + $ PYTHONPATH=src python -m websockets ws://localhost:8080/ + Connected to ws://localhost:8080/. + > Hello! + < Hello! + Connection closed: 1000 (OK). diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/howto/heroku.rst b/tests/wpt/tests/tools/third_party/websockets/docs/howto/heroku.rst new file mode 100644 index 00000000000..a97d2e7ce03 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/howto/heroku.rst @@ -0,0 +1,183 @@ +Deploy to Heroku +================ + +This guide describes how to deploy a websockets server to Heroku_. The same +principles should apply to other Platform as a Service providers. + +.. _Heroku: https://www.heroku.com/ + +.. admonition:: Heroku no longer offers a free tier. + :class: attention + + When this tutorial was written, in September 2021, Heroku offered a free + tier where a websockets app could run at no cost. In November 2022, Heroku + removed the free tier, making it impossible to maintain this document. As a + consequence, it isn't updated anymore and may be removed in the future. + +We're going to deploy a very simple app. The process would be identical for a +more realistic app. + +Create repository +----------------- + +Deploying to Heroku requires a git repository. Let's initialize one: + +.. code-block:: console + + $ mkdir websockets-echo + $ cd websockets-echo + $ git init -b main + Initialized empty Git repository in websockets-echo/.git/ + $ git commit --allow-empty -m "Initial commit." + [main (root-commit) 1e7947d] Initial commit. + +Create application +------------------ + +Here's the implementation of the app, an echo server. Save it in a file called +``app.py``: + +.. literalinclude:: ../../example/deployment/heroku/app.py + :language: python + +Heroku expects the server to `listen on a specific port`_, which is provided +in the ``$PORT`` environment variable. The app reads it and passes it to +:func:`~websockets.server.serve`. + +.. _listen on a specific port: https://devcenter.heroku.com/articles/preparing-a-codebase-for-heroku-deployment#4-listen-on-the-correct-port + +Heroku sends a ``SIGTERM`` signal to all processes when `shutting down a +dyno`_. When the app receives this signal, it closes connections and exits +cleanly. + +.. _shutting down a dyno: https://devcenter.heroku.com/articles/dynos#shutdown + +Create a ``requirements.txt`` file containing this line to declare a dependency +on websockets: + +.. literalinclude:: ../../example/deployment/heroku/requirements.txt + :language: text + +Create a ``Procfile``. + +.. literalinclude:: ../../example/deployment/heroku/Procfile + +This tells Heroku how to run the app. + +Confirm that you created the correct files and commit them to git: + +.. code-block:: console + + $ ls + Procfile app.py requirements.txt + $ git add . + $ git commit -m "Initial implementation." + [main 8418c62] Initial implementation. + 3 files changed, 32 insertions(+) + create mode 100644 Procfile + create mode 100644 app.py + create mode 100644 requirements.txt + +The app is ready. Let's deploy it! + +Deploy application +------------------ + +Follow the instructions_ to install the Heroku CLI, if you haven't done that +yet. + +.. _instructions: https://devcenter.heroku.com/articles/getting-started-with-python#set-up + +Sign up or log in to Heroku. + +Create a Heroku app — you'll have to pick a different name because I'm already +using ``websockets-echo``: + +.. code-block:: console + + $ heroku create websockets-echo + Creating ⬢ websockets-echo... done + https://websockets-echo.herokuapp.com/ | https://git.heroku.com/websockets-echo.git + +.. code-block:: console + + $ git push heroku + + ... lots of output... + + remote: -----> Launching... + remote: Released v1 + remote: https://websockets-echo.herokuapp.com/ deployed to Heroku + remote: + remote: Verifying deploy... done. + To https://git.heroku.com/websockets-echo.git + * [new branch] main -> main + +Validate deployment +------------------- + +Let's confirm that your application is running as expected. + +Since it's a WebSocket server, you need a WebSocket client, such as the +interactive client that comes with websockets. + +If you're currently building a websockets server, perhaps you're already in a +virtualenv where websockets is installed. If not, you can install it in a new +virtualenv as follows: + +.. code-block:: console + + $ python -m venv websockets-client + $ . websockets-client/bin/activate + $ pip install websockets + +Connect the interactive client — you must replace ``websockets-echo`` with the +name of your Heroku app in this command: + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.herokuapp.com/ + Connected to wss://websockets-echo.herokuapp.com/. + > + +Great! Your app is running! + +Once you're connected, you can send any message and the server will echo it, +or press Ctrl-D to terminate the connection: + +.. code-block:: console + + > Hello! + < Hello! + Connection closed: 1000 (OK). + +You can also confirm that your application shuts down gracefully. + +Connect an interactive client again — remember to replace ``websockets-echo`` +with your app: + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.herokuapp.com/ + Connected to wss://websockets-echo.herokuapp.com/. + > + +In another shell, restart the app — again, replace ``websockets-echo`` with your +app: + +.. code-block:: console + + $ heroku dyno:restart -a websockets-echo + Restarting dynos on ⬢ websockets-echo... done + +Go back to the first shell. The connection is closed with code 1001 (going +away). + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.herokuapp.com/ + Connected to wss://websockets-echo.herokuapp.com/. + Connection closed: 1001 (going away). + +If graceful shutdown wasn't working, the server wouldn't perform a closing +handshake and the connection would be closed with code 1006 (abnormal closure). diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/howto/index.rst b/tests/wpt/tests/tools/third_party/websockets/docs/howto/index.rst new file mode 100644 index 00000000000..ddbe67d3ae2 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/howto/index.rst @@ -0,0 +1,56 @@ +How-to guides +============= + +In a hurry? Check out these examples. + +.. toctree:: + :titlesonly: + + quickstart + +If you're stuck, perhaps you'll find the answer here. + +.. toctree:: + :titlesonly: + + cheatsheet + patterns + autoreload + +This guide will help you integrate websockets into a broader system. + +.. toctree:: + :titlesonly: + + django + +The WebSocket protocol makes provisions for extending or specializing its +features, which websockets supports fully. + +.. toctree:: + :titlesonly: + + extensions + +.. _deployment-howto: + +Once your application is ready, learn how to deploy it on various platforms. + +.. toctree:: + :titlesonly: + + render + fly + heroku + kubernetes + supervisor + nginx + haproxy + +If you're integrating the Sans-I/O layer of websockets into a library, rather +than building an application with websockets, follow this guide. + +.. toctree:: + :maxdepth: 2 + + sansio diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/howto/kubernetes.rst b/tests/wpt/tests/tools/third_party/websockets/docs/howto/kubernetes.rst new file mode 100644 index 00000000000..064a6ac4d58 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/howto/kubernetes.rst @@ -0,0 +1,215 @@ +Deploy to Kubernetes +==================== + +This guide describes how to deploy a websockets server to Kubernetes_. It +assumes familiarity with Docker and Kubernetes. + +We're going to deploy a simple app to a local Kubernetes cluster and to ensure +that it scales as expected. + +In a more realistic context, you would follow your organization's practices +for deploying to Kubernetes, but you would apply the same principles as far as +websockets is concerned. + +.. _Kubernetes: https://kubernetes.io/ + +.. _containerize-application: + +Containerize application +------------------------ + +Here's the app we're going to deploy. Save it in a file called +``app.py``: + +.. literalinclude:: ../../example/deployment/kubernetes/app.py + +This is an echo server with one twist: every message blocks the server for +100ms, which creates artificial starvation of CPU time. This makes it easier +to saturate the server for load testing. + +The app exposes a health check on ``/healthz``. It also provides two other +endpoints for testing purposes: ``/inemuri`` will make the app unresponsive +for 10 seconds and ``/seppuku`` will terminate it. + +The quest for the perfect Python container image is out of scope of this +guide, so we'll go for the simplest possible configuration instead: + +.. literalinclude:: ../../example/deployment/kubernetes/Dockerfile + +After saving this ``Dockerfile``, build the image: + +.. code-block:: console + + $ docker build -t websockets-test:1.0 . + +Test your image by running: + +.. code-block:: console + + $ docker run --name run-websockets-test --publish 32080:80 --rm \ + websockets-test:1.0 + +Then, in another shell, in a virtualenv where websockets is installed, connect +to the app and check that it echoes anything you send: + +.. code-block:: console + + $ python -m websockets ws://localhost:32080/ + Connected to ws://localhost:32080/. + > Hey there! + < Hey there! + > + +Now, in yet another shell, stop the app with: + +.. code-block:: console + + $ docker kill -s TERM run-websockets-test + +Going to the shell where you connected to the app, you can confirm that it +shut down gracefully: + +.. code-block:: console + + $ python -m websockets ws://localhost:32080/ + Connected to ws://localhost:32080/. + > Hey there! + < Hey there! + Connection closed: 1001 (going away). + +If it didn't, you'd get code 1006 (abnormal closure). + +Deploy application +------------------ + +Configuring Kubernetes is even further beyond the scope of this guide, so +we'll use a basic configuration for testing, with just one Service_ and one +Deployment_: + +.. literalinclude:: ../../example/deployment/kubernetes/deployment.yaml + +For local testing, a service of type NodePort_ is good enough. For deploying +to production, you would configure an Ingress_. + +.. _Service: https://kubernetes.io/docs/concepts/services-networking/service/ +.. _Deployment: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/ +.. _NodePort: https://kubernetes.io/docs/concepts/services-networking/service/#nodeport +.. _Ingress: https://kubernetes.io/docs/concepts/services-networking/ingress/ + +After saving this to a file called ``deployment.yaml``, you can deploy: + +.. code-block:: console + + $ kubectl apply -f deployment.yaml + service/websockets-test created + deployment.apps/websockets-test created + +Now you have a deployment with one pod running: + +.. code-block:: console + + $ kubectl get deployment websockets-test + NAME READY UP-TO-DATE AVAILABLE AGE + websockets-test 1/1 1 1 10s + $ kubectl get pods -l app=websockets-test + NAME READY STATUS RESTARTS AGE + websockets-test-86b48f4bb7-nltfh 1/1 Running 0 10s + +You can connect to the service — press Ctrl-D to exit: + +.. code-block:: console + + $ python -m websockets ws://localhost:32080/ + Connected to ws://localhost:32080/. + Connection closed: 1000 (OK). + +Validate deployment +------------------- + +First, let's ensure the liveness probe works by making the app unresponsive: + +.. code-block:: console + + $ curl http://localhost:32080/inemuri + Sleeping for 10s + +Since we have only one pod, we know that this pod will go to sleep. + +The liveness probe is configured to run every second. By default, liveness +probes time out after one second and have a threshold of three failures. +Therefore Kubernetes should restart the pod after at most 5 seconds. + +Indeed, after a few seconds, the pod reports a restart: + +.. code-block:: console + + $ kubectl get pods -l app=websockets-test + NAME READY STATUS RESTARTS AGE + websockets-test-86b48f4bb7-nltfh 1/1 Running 1 42s + +Next, let's take it one step further and crash the app: + +.. code-block:: console + + $ curl http://localhost:32080/seppuku + Terminating + +The pod reports a second restart: + +.. code-block:: console + + $ kubectl get pods -l app=websockets-test + NAME READY STATUS RESTARTS AGE + websockets-test-86b48f4bb7-nltfh 1/1 Running 2 72s + +All good — Kubernetes delivers on its promise to keep our app alive! + +Scale deployment +---------------- + +Of course, Kubernetes is for scaling. Let's scale — modestly — to 10 pods: + +.. code-block:: console + + $ kubectl scale deployment.apps/websockets-test --replicas=10 + deployment.apps/websockets-test scaled + +After a few seconds, we have 10 pods running: + +.. code-block:: console + + $ kubectl get deployment websockets-test + NAME READY UP-TO-DATE AVAILABLE AGE + websockets-test 10/10 10 10 10m + +Now let's generate load. We'll use this script: + +.. literalinclude:: ../../example/deployment/kubernetes/benchmark.py + +We'll connect 500 clients in parallel, meaning 50 clients per pod, and have +each client send 6 messages. Since the app blocks for 100ms before responding, +if connections are perfectly distributed, we expect a total run time slightly +over 50 * 6 * 0.1 = 30 seconds. + +Let's try it: + +.. code-block:: console + + $ ulimit -n 512 + $ time python benchmark.py 500 6 + python benchmark.py 500 6 2.40s user 0.51s system 7% cpu 36.471 total + +A total runtime of 36 seconds is in the right ballpark. Repeating this +experiment with other parameters shows roughly consistent results, with the +high variability you'd expect from a quick benchmark without any effort to +stabilize the test setup. + +Finally, we can scale back to one pod. + +.. code-block:: console + + $ kubectl scale deployment.apps/websockets-test --replicas=1 + deployment.apps/websockets-test scaled + $ kubectl get deployment websockets-test + NAME READY UP-TO-DATE AVAILABLE AGE + websockets-test 1/1 1 1 15m diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/howto/nginx.rst b/tests/wpt/tests/tools/third_party/websockets/docs/howto/nginx.rst new file mode 100644 index 00000000000..30545fbc7d1 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/howto/nginx.rst @@ -0,0 +1,84 @@ +Deploy behind nginx +=================== + +This guide demonstrates a way to load balance connections across multiple +websockets server processes running on the same machine with nginx_. + +We'll run server processes with Supervisor as described in :doc:`this guide +<supervisor>`. + +.. _nginx: https://nginx.org/ + +Run server processes +-------------------- + +Save this app to ``app.py``: + +.. literalinclude:: ../../example/deployment/nginx/app.py + :emphasize-lines: 21,23 + +We'd like to nginx to connect to websockets servers via Unix sockets in order +to avoid the overhead of TCP for communicating between processes running in +the same OS. + +We start the app with :func:`~websockets.server.unix_serve`. Each server +process listens on a different socket thanks to an environment variable set +by Supervisor to a different value. + +Save this configuration to ``supervisord.conf``: + +.. literalinclude:: ../../example/deployment/nginx/supervisord.conf + +This configuration runs four instances of the app. + +Install Supervisor and run it: + +.. code-block:: console + + $ supervisord -c supervisord.conf -n + +Configure and run nginx +----------------------- + +Here's a simple nginx configuration to load balance connections across four +processes: + +.. literalinclude:: ../../example/deployment/nginx/nginx.conf + +We set ``daemon off`` so we can run nginx in the foreground for testing. + +Then we combine the `WebSocket proxying`_ and `load balancing`_ guides: + +* The WebSocket protocol requires HTTP/1.1. We must set the HTTP protocol + version to 1.1, else nginx defaults to HTTP/1.0 for proxying. + +* The WebSocket handshake involves the ``Connection`` and ``Upgrade`` HTTP + headers. We must pass them to the upstream explicitly, else nginx drops + them because they're hop-by-hop headers. + + We deviate from the `WebSocket proxying`_ guide because its example adds a + ``Connection: Upgrade`` header to every upstream request, even if the + original request didn't contain that header. + +* In the upstream configuration, we set the load balancing method to + ``least_conn`` in order to balance the number of active connections across + servers. This is best for long running connections. + +.. _WebSocket proxying: http://nginx.org/en/docs/http/websocket.html +.. _load balancing: http://nginx.org/en/docs/http/load_balancing.html + +Save the configuration to ``nginx.conf``, install nginx, and run it: + +.. code-block:: console + + $ nginx -c nginx.conf -p . + +You can confirm that nginx proxies connections properly: + +.. code-block:: console + + $ PYTHONPATH=src python -m websockets ws://localhost:8080/ + Connected to ws://localhost:8080/. + > Hello! + < Hello! + Connection closed: 1000 (OK). diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/howto/patterns.rst b/tests/wpt/tests/tools/third_party/websockets/docs/howto/patterns.rst new file mode 100644 index 00000000000..c6f325d2137 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/howto/patterns.rst @@ -0,0 +1,110 @@ +Patterns +======== + +.. currentmodule:: websockets + +Here are typical patterns for processing messages in a WebSocket server or +client. You will certainly implement some of them in your application. + +This page gives examples of connection handlers for a server. However, they're +also applicable to a client, simply by assuming that ``websocket`` is a +connection created with :func:`~client.connect`. + +WebSocket connections are long-lived. You will usually write a loop to process +several messages during the lifetime of a connection. + +Consumer +-------- + +To receive messages from the WebSocket connection:: + + async def consumer_handler(websocket): + async for message in websocket: + await consumer(message) + +In this example, ``consumer()`` is a coroutine implementing your business +logic for processing a message received on the WebSocket connection. Each +message may be :class:`str` or :class:`bytes`. + +Iteration terminates when the client disconnects. + +Producer +-------- + +To send messages to the WebSocket connection:: + + async def producer_handler(websocket): + while True: + message = await producer() + await websocket.send(message) + +In this example, ``producer()`` is a coroutine implementing your business +logic for generating the next message to send on the WebSocket connection. +Each message must be :class:`str` or :class:`bytes`. + +Iteration terminates when the client disconnects +because :meth:`~server.WebSocketServerProtocol.send` raises a +:exc:`~exceptions.ConnectionClosed` exception, +which breaks out of the ``while True`` loop. + +Consumer and producer +--------------------- + +You can receive and send messages on the same WebSocket connection by +combining the consumer and producer patterns. This requires running two tasks +in parallel:: + + async def handler(websocket): + await asyncio.gather( + consumer_handler(websocket), + producer_handler(websocket), + ) + +If a task terminates, :func:`~asyncio.gather` doesn't cancel the other task. +This can result in a situation where the producer keeps running after the +consumer finished, which may leak resources. + +Here's a way to exit and close the WebSocket connection as soon as a task +terminates, after canceling the other task:: + + async def handler(websocket): + consumer_task = asyncio.create_task(consumer_handler(websocket)) + producer_task = asyncio.create_task(producer_handler(websocket)) + done, pending = await asyncio.wait( + [consumer_task, producer_task], + return_when=asyncio.FIRST_COMPLETED, + ) + for task in pending: + task.cancel() + +Registration +------------ + +To keep track of currently connected clients, you can register them when they +connect and unregister them when they disconnect:: + + connected = set() + + async def handler(websocket): + # Register. + connected.add(websocket) + try: + # Broadcast a message to all connected clients. + websockets.broadcast(connected, "Hello!") + await asyncio.sleep(10) + finally: + # Unregister. + connected.remove(websocket) + +This example maintains the set of connected clients in memory. This works as +long as you run a single process. It doesn't scale to multiple processes. + +Publish–subscribe +----------------- + +If you plan to run multiple processes and you want to communicate updates +between processes, then you must deploy a messaging system. You may find +publish-subscribe functionality useful. + +A complete implementation of this idea with Redis is described in +the :doc:`Django integration guide <../howto/django>`. diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/howto/quickstart.rst b/tests/wpt/tests/tools/third_party/websockets/docs/howto/quickstart.rst new file mode 100644 index 00000000000..ab870952c17 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/howto/quickstart.rst @@ -0,0 +1,170 @@ +Quick start +=========== + +.. currentmodule:: websockets + +Here are a few examples to get you started quickly with websockets. + +Say "Hello world!" +------------------ + +Here's a WebSocket server. + +It receives a name from the client, sends a greeting, and closes the connection. + +.. literalinclude:: ../../example/quickstart/server.py + :caption: server.py + :language: python + :linenos: + +:func:`~server.serve` executes the connection handler coroutine ``hello()`` +once for each WebSocket connection. It closes the WebSocket connection when +the handler returns. + +Here's a corresponding WebSocket client. + +It sends a name to the server, receives a greeting, and closes the connection. + +.. literalinclude:: ../../example/quickstart/client.py + :caption: client.py + :language: python + :linenos: + +Using :func:`~client.connect` as an asynchronous context manager ensures the +WebSocket connection is closed. + +.. _secure-server-example: + +Encrypt connections +------------------- + +Secure WebSocket connections improve confidentiality and also reliability +because they reduce the risk of interference by bad proxies. + +The ``wss`` protocol is to ``ws`` what ``https`` is to ``http``. The +connection is encrypted with TLS_ (Transport Layer Security). ``wss`` +requires certificates like ``https``. + +.. _TLS: https://developer.mozilla.org/en-US/docs/Web/Security/Transport_Layer_Security + +.. admonition:: TLS vs. SSL + :class: tip + + TLS is sometimes referred to as SSL (Secure Sockets Layer). SSL was an + earlier encryption protocol; the name stuck. + +Here's how to adapt the server to encrypt connections. You must download +:download:`localhost.pem <../../example/quickstart/localhost.pem>` and save it +in the same directory as ``server_secure.py``. + +.. literalinclude:: ../../example/quickstart/server_secure.py + :caption: server_secure.py + :language: python + :linenos: + +Here's how to adapt the client similarly. + +.. literalinclude:: ../../example/quickstart/client_secure.py + :caption: client_secure.py + :language: python + :linenos: + +In this example, the client needs a TLS context because the server uses a +self-signed certificate. + +When connecting to a secure WebSocket server with a valid certificate — any +certificate signed by a CA that your Python installation trusts — you can +simply pass ``ssl=True`` to :func:`~client.connect`. + +.. admonition:: Configure the TLS context securely + :class: attention + + This example demonstrates the ``ssl`` argument with a TLS certificate shared + between the client and the server. This is a simplistic setup. + + Please review the advice and security considerations in the documentation of + the :mod:`ssl` module to configure the TLS context securely. + +Connect from a browser +---------------------- + +The WebSocket protocol was invented for the web — as the name says! + +Here's how to connect to a WebSocket server from a browser. + +Run this script in a console: + +.. literalinclude:: ../../example/quickstart/show_time.py + :caption: show_time.py + :language: python + :linenos: + +Save this file as ``show_time.html``: + +.. literalinclude:: ../../example/quickstart/show_time.html + :caption: show_time.html + :language: html + :linenos: + +Save this file as ``show_time.js``: + +.. literalinclude:: ../../example/quickstart/show_time.js + :caption: show_time.js + :language: js + :linenos: + +Then, open ``show_time.html`` in several browsers. Clocks tick irregularly. + +Broadcast messages +------------------ + +Let's change the previous example to send the same timestamps to all browsers, +instead of generating independent sequences for each client. + +Stop the previous script if it's still running and run this script in a console: + +.. literalinclude:: ../../example/quickstart/show_time_2.py + :caption: show_time_2.py + :language: python + :linenos: + +Refresh ``show_time.html`` in all browsers. Clocks tick in sync. + +Manage application state +------------------------ + +A WebSocket server can receive events from clients, process them to update the +application state, and broadcast the updated state to all connected clients. + +Here's an example where any client can increment or decrement a counter. The +concurrency model of :mod:`asyncio` guarantees that updates are serialized. + +Run this script in a console: + +.. literalinclude:: ../../example/quickstart/counter.py + :caption: counter.py + :language: python + :linenos: + +Save this file as ``counter.html``: + +.. literalinclude:: ../../example/quickstart/counter.html + :caption: counter.html + :language: html + :linenos: + +Save this file as ``counter.css``: + +.. literalinclude:: ../../example/quickstart/counter.css + :caption: counter.css + :language: css + :linenos: + +Save this file as ``counter.js``: + +.. literalinclude:: ../../example/quickstart/counter.js + :caption: counter.js + :language: js + :linenos: + +Then open ``counter.html`` file in several browsers and play with [+] and [-]. diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/howto/render.rst b/tests/wpt/tests/tools/third_party/websockets/docs/howto/render.rst new file mode 100644 index 00000000000..70bf8c376c4 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/howto/render.rst @@ -0,0 +1,172 @@ +Deploy to Render +================ + +This guide describes how to deploy a websockets server to Render_. + +.. _Render: https://render.com/ + +.. admonition:: The free plan of Render is sufficient for trying this guide. + :class: tip + + However, on a `free plan`__, connections are dropped after five minutes, + which is quite short for WebSocket application. + + __ https://render.com/docs/free + +We're going to deploy a very simple app. The process would be identical for a +more realistic app. + +Create repository +----------------- + +Deploying to Render requires a git repository. Let's initialize one: + +.. code-block:: console + + $ mkdir websockets-echo + $ cd websockets-echo + $ git init -b main + Initialized empty Git repository in websockets-echo/.git/ + $ git commit --allow-empty -m "Initial commit." + [main (root-commit) 816c3b1] Initial commit. + +Render requires the git repository to be hosted at GitHub or GitLab. + +Sign up or log in to GitHub. Create a new repository named ``websockets-echo``. +Don't enable any of the initialization options offered by GitHub. Then, follow +instructions for pushing an existing repository from the command line. + +After pushing, refresh your repository's homepage on GitHub. You should see an +empty repository with an empty initial commit. + +Create application +------------------ + +Here's the implementation of the app, an echo server. Save it in a file called +``app.py``: + +.. literalinclude:: ../../example/deployment/render/app.py + :language: python + +This app implements requirements for `zero downtime deploys`_: + +* it provides a health check at ``/healthz``; +* it closes connections and exits cleanly when it receives a ``SIGTERM`` signal. + +.. _zero downtime deploys: https://render.com/docs/deploys#zero-downtime-deploys + +Create a ``requirements.txt`` file containing this line to declare a dependency +on websockets: + +.. literalinclude:: ../../example/deployment/render/requirements.txt + :language: text + +Confirm that you created the correct files and commit them to git: + +.. code-block:: console + + $ ls + app.py requirements.txt + $ git add . + $ git commit -m "Initial implementation." + [main f26bf7f] Initial implementation. + 2 files changed, 37 insertions(+) + create mode 100644 app.py + create mode 100644 requirements.txt + +Push the changes to GitHub: + +.. code-block:: console + + $ git push + ... + To github.com:<username>/websockets-echo.git + 816c3b1..f26bf7f main -> main + +The app is ready. Let's deploy it! + +Deploy application +------------------ + +Sign up or log in to Render. + +Create a new web service. Connect the git repository that you just created. + +Then, finalize the configuration of your app as follows: + +* **Name**: websockets-echo +* **Start Command**: ``python app.py`` + +If you're just experimenting, select the free plan. Create the web service. + +To configure the health check, go to Settings, scroll down to Health & Alerts, +and set: + +* **Health Check Path**: /healthz + +This triggers a new deployment. + +Validate deployment +------------------- + +Let's confirm that your application is running as expected. + +Since it's a WebSocket server, you need a WebSocket client, such as the +interactive client that comes with websockets. + +If you're currently building a websockets server, perhaps you're already in a +virtualenv where websockets is installed. If not, you can install it in a new +virtualenv as follows: + +.. code-block:: console + + $ python -m venv websockets-client + $ . websockets-client/bin/activate + $ pip install websockets + +Connect the interactive client — you must replace ``websockets-echo`` with the +name of your Render app in this command: + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.onrender.com/ + Connected to wss://websockets-echo.onrender.com/. + > + +Great! Your app is running! + +Once you're connected, you can send any message and the server will echo it, +or press Ctrl-D to terminate the connection: + +.. code-block:: console + + > Hello! + < Hello! + Connection closed: 1000 (OK). + +You can also confirm that your application shuts down gracefully when you deploy +a new version. Due to limitations of Render's free plan, you must upgrade to a +paid plan before you perform this test. + +Connect an interactive client again — remember to replace ``websockets-echo`` +with your app: + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.onrender.com/ + Connected to wss://websockets-echo.onrender.com/. + > + +Trigger a new deployment with Manual Deploy > Deploy latest commit. When the +deployment completes, the connection is closed with code 1001 (going away). + +.. code-block:: console + + $ python -m websockets wss://websockets-echo.onrender.com/ + Connected to wss://websockets-echo.onrender.com/. + Connection closed: 1001 (going away). + +If graceful shutdown wasn't working, the server wouldn't perform a closing +handshake and the connection would be closed with code 1006 (abnormal closure). + +Remember to downgrade to a free plan if you upgraded just for testing this feature. diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/howto/sansio.rst b/tests/wpt/tests/tools/third_party/websockets/docs/howto/sansio.rst new file mode 100644 index 00000000000..d41519ff091 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/howto/sansio.rst @@ -0,0 +1,322 @@ +Integrate the Sans-I/O layer +============================ + +.. currentmodule:: websockets + +This guide explains how to integrate the `Sans-I/O`_ layer of websockets to +add support for WebSocket in another library. + +.. _Sans-I/O: https://sans-io.readthedocs.io/ + +As a prerequisite, you should decide how you will handle network I/O and +asynchronous control flow. + +Your integration layer will provide an API for the application on one side, +will talk to the network on the other side, and will rely on websockets to +implement the protocol in the middle. + +.. image:: ../topics/data-flow.svg + :align: center + +Opening a connection +-------------------- + +Client-side +........... + +If you're building a client, parse the URI you'd like to connect to:: + + from websockets.uri import parse_uri + + wsuri = parse_uri("ws://example.com/") + +Open a TCP connection to ``(wsuri.host, wsuri.port)`` and perform a TLS +handshake if ``wsuri.secure`` is :obj:`True`. + +Initialize a :class:`~client.ClientProtocol`:: + + from websockets.client import ClientProtocol + + protocol = ClientProtocol(wsuri) + +Create a WebSocket handshake request +with :meth:`~client.ClientProtocol.connect` and send it +with :meth:`~client.ClientProtocol.send_request`:: + + request = protocol.connect() + protocol.send_request(request) + +Then, call :meth:`~protocol.Protocol.data_to_send` and send its output to +the network, as described in `Send data`_ below. + +Once you receive enough data, as explained in `Receive data`_ below, the first +event returned by :meth:`~protocol.Protocol.events_received` is the WebSocket +handshake response. + +When the handshake fails, the reason is available in +:attr:`~client.ClientProtocol.handshake_exc`:: + + if protocol.handshake_exc is not None: + raise protocol.handshake_exc + +Else, the WebSocket connection is open. + +A WebSocket client API usually performs the handshake then returns a wrapper +around the network socket and the :class:`~client.ClientProtocol`. + +Server-side +........... + +If you're building a server, accept network connections from clients and +perform a TLS handshake if desired. + +For each connection, initialize a :class:`~server.ServerProtocol`:: + + from websockets.server import ServerProtocol + + protocol = ServerProtocol() + +Once you receive enough data, as explained in `Receive data`_ below, the first +event returned by :meth:`~protocol.Protocol.events_received` is the WebSocket +handshake request. + +Create a WebSocket handshake response +with :meth:`~server.ServerProtocol.accept` and send it +with :meth:`~server.ServerProtocol.send_response`:: + + response = protocol.accept(request) + protocol.send_response(response) + +Alternatively, you may reject the WebSocket handshake and return an HTTP +response with :meth:`~server.ServerProtocol.reject`:: + + response = protocol.reject(status, explanation) + protocol.send_response(response) + +Then, call :meth:`~protocol.Protocol.data_to_send` and send its output to +the network, as described in `Send data`_ below. + +Even when you call :meth:`~server.ServerProtocol.accept`, the WebSocket +handshake may fail if the request is incorrect or unsupported. + +When the handshake fails, the reason is available in +:attr:`~server.ServerProtocol.handshake_exc`:: + + if protocol.handshake_exc is not None: + raise protocol.handshake_exc + +Else, the WebSocket connection is open. + +A WebSocket server API usually builds a wrapper around the network socket and +the :class:`~server.ServerProtocol`. Then it invokes a connection handler that +accepts the wrapper in argument. + +It may also provide a way to close all connections and to shut down the server +gracefully. + +Going forwards, this guide focuses on handling an individual connection. + +From the network to the application +----------------------------------- + +Go through the five steps below until you reach the end of the data stream. + +Receive data +............ + +When receiving data from the network, feed it to the protocol's +:meth:`~protocol.Protocol.receive_data` method. + +When reaching the end of the data stream, call the protocol's +:meth:`~protocol.Protocol.receive_eof` method. + +For example, if ``sock`` is a :obj:`~socket.socket`:: + + try: + data = sock.recv(65536) + except OSError: # socket closed + data = b"" + if data: + protocol.receive_data(data) + else: + protocol.receive_eof() + +These methods aren't expected to raise exceptions — unless you call them again +after calling :meth:`~protocol.Protocol.receive_eof`, which is an error. +(If you get an exception, please file a bug!) + +Send data +......... + +Then, call :meth:`~protocol.Protocol.data_to_send` and send its output to +the network:: + + for data in protocol.data_to_send(): + if data: + sock.sendall(data) + else: + sock.shutdown(socket.SHUT_WR) + +The empty bytestring signals the end of the data stream. When you see it, you +must half-close the TCP connection. + +Sending data right after receiving data is necessary because websockets +responds to ping frames, close frames, and incorrect inputs automatically. + +Expect TCP connection to close +.............................. + +Closing a WebSocket connection normally involves a two-way WebSocket closing +handshake. Then, regardless of whether the closure is normal or abnormal, the +server starts the four-way TCP closing handshake. If the network fails at the +wrong point, you can end up waiting until the TCP timeout, which is very long. + +To prevent dangling TCP connections when you expect the end of the data stream +but you never reach it, call :meth:`~protocol.Protocol.close_expected` +and, if it returns :obj:`True`, schedule closing the TCP connection after a +short timeout:: + + # start a new execution thread to run this code + sleep(10) + sock.close() # does nothing if the socket is already closed + +If the connection is still open when the timeout elapses, closing the socket +makes the execution thread that reads from the socket reach the end of the +data stream, possibly with an exception. + +Close TCP connection +.................... + +If you called :meth:`~protocol.Protocol.receive_eof`, close the TCP +connection now. This is a clean closure because the receive buffer is empty. + +After :meth:`~protocol.Protocol.receive_eof` signals the end of the read +stream, :meth:`~protocol.Protocol.data_to_send` always signals the end of +the write stream, unless it already ended. So, at this point, the TCP +connection is already half-closed. The only reason for closing it now is to +release resources related to the socket. + +Now you can exit the loop relaying data from the network to the application. + +Receive events +.............. + +Finally, call :meth:`~protocol.Protocol.events_received` to obtain events +parsed from the data provided to :meth:`~protocol.Protocol.receive_data`:: + + events = connection.events_received() + +The first event will be the WebSocket opening handshake request or response. +See `Opening a connection`_ above for details. + +All later events are WebSocket frames. There are two types of frames: + +* Data frames contain messages transferred over the WebSocket connections. You + should provide them to the application. See `Fragmentation`_ below for + how to reassemble messages from frames. +* Control frames provide information about the connection's state. The main + use case is to expose an abstraction over ping and pong to the application. + Keep in mind that websockets responds to ping frames and close frames + automatically. Don't duplicate this functionality! + +From the application to the network +----------------------------------- + +The connection object provides one method for each type of WebSocket frame. + +For sending a data frame: + +* :meth:`~protocol.Protocol.send_continuation` +* :meth:`~protocol.Protocol.send_text` +* :meth:`~protocol.Protocol.send_binary` + +These methods raise :exc:`~exceptions.ProtocolError` if you don't set +the :attr:`FIN <websockets.frames.Frame.fin>` bit correctly in fragmented +messages. + +For sending a control frame: + +* :meth:`~protocol.Protocol.send_close` +* :meth:`~protocol.Protocol.send_ping` +* :meth:`~protocol.Protocol.send_pong` + +:meth:`~protocol.Protocol.send_close` initiates the closing handshake. +See `Closing a connection`_ below for details. + +If you encounter an unrecoverable error and you must fail the WebSocket +connection, call :meth:`~protocol.Protocol.fail`. + +After any of the above, call :meth:`~protocol.Protocol.data_to_send` and +send its output to the network, as shown in `Send data`_ above. + +If you called :meth:`~protocol.Protocol.send_close` +or :meth:`~protocol.Protocol.fail`, you expect the end of the data +stream. You should follow the process described in `Close TCP connection`_ +above in order to prevent dangling TCP connections. + +Closing a connection +-------------------- + +Under normal circumstances, when a server wants to close the TCP connection: + +* it closes the write side; +* it reads until the end of the stream, because it expects the client to close + the read side; +* it closes the socket. + +When a client wants to close the TCP connection: + +* it reads until the end of the stream, because it expects the server to close + the read side; +* it closes the write side; +* it closes the socket. + +Applying the rules described earlier in this document gives the intended +result. As a reminder, the rules are: + +* When :meth:`~protocol.Protocol.data_to_send` returns the empty + bytestring, close the write side of the TCP connection. +* When you reach the end of the read stream, close the TCP connection. +* When :meth:`~protocol.Protocol.close_expected` returns :obj:`True`, if + you don't reach the end of the read stream quickly, close the TCP connection. + +Fragmentation +------------- + +WebSocket messages may be fragmented. Since this is a protocol-level concern, +you may choose to reassemble fragmented messages before handing them over to +the application. + +To reassemble a message, read data frames until you get a frame where +the :attr:`FIN <websockets.frames.Frame.fin>` bit is set, then concatenate +the payloads of all frames. + +You will never receive an inconsistent sequence of frames because websockets +raises a :exc:`~exceptions.ProtocolError` and fails the connection when this +happens. However, you may receive an incomplete sequence if the connection +drops in the middle of a fragmented message. + +Tips +---- + +Serialize operations +.................... + +The Sans-I/O layer expects to run sequentially. If your interact with it from +multiple threads or coroutines, you must ensure correct serialization. This +should happen automatically in a cooperative multitasking environment. + +However, you still have to make sure you don't break this property by +accident. For example, serialize writes to the network +when :meth:`~protocol.Protocol.data_to_send` returns multiple values to +prevent concurrent writes from interleaving incorrectly. + +Avoid buffers +............. + +The Sans-I/O layer doesn't do any buffering. It makes events available in +:meth:`~protocol.Protocol.events_received` as soon as they're received. + +You should make incoming messages available to the application immediately and +stop further processing until the application fetches them. This will usually +result in the best performance. diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/howto/supervisor.rst b/tests/wpt/tests/tools/third_party/websockets/docs/howto/supervisor.rst new file mode 100644 index 00000000000..5eefc7711b8 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/howto/supervisor.rst @@ -0,0 +1,131 @@ +Deploy with Supervisor +====================== + +This guide proposes a simple way to deploy a websockets server directly on a +Linux or BSD operating system. + +We'll configure Supervisor_ to run several server processes and to restart +them if needed. + +.. _Supervisor: http://supervisord.org/ + +We'll bind all servers to the same port. The OS will take care of balancing +connections. + +Create and activate a virtualenv: + +.. code-block:: console + + $ python -m venv supervisor-websockets + $ . supervisor-websockets/bin/activate + +Install websockets and Supervisor: + +.. code-block:: console + + $ pip install websockets + $ pip install supervisor + +Save this app to a file called ``app.py``: + +.. literalinclude:: ../../example/deployment/supervisor/app.py + +This is an echo server with two features added for the purpose of this guide: + +* It shuts down gracefully when receiving a ``SIGTERM`` signal; +* It enables the ``reuse_port`` option of :meth:`~asyncio.loop.create_server`, + which in turns sets ``SO_REUSEPORT`` on the accept socket. + +Save this Supervisor configuration to ``supervisord.conf``: + +.. literalinclude:: ../../example/deployment/supervisor/supervisord.conf + +This is the minimal configuration required to keep four instances of the app +running, restarting them if they exit. + +Now start Supervisor in the foreground: + +.. code-block:: console + + $ supervisord -c supervisord.conf -n + INFO Increased RLIMIT_NOFILE limit to 1024 + INFO supervisord started with pid 43596 + INFO spawned: 'websockets-test_00' with pid 43597 + INFO spawned: 'websockets-test_01' with pid 43598 + INFO spawned: 'websockets-test_02' with pid 43599 + INFO spawned: 'websockets-test_03' with pid 43600 + INFO success: websockets-test_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) + INFO success: websockets-test_01 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) + INFO success: websockets-test_02 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) + INFO success: websockets-test_03 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) + +In another shell, after activating the virtualenv, we can connect to the app — +press Ctrl-D to exit: + +.. code-block:: console + + $ python -m websockets ws://localhost:8080/ + Connected to ws://localhost:8080/. + > Hello! + < Hello! + Connection closed: 1000 (OK). + +Look at the pid of an instance of the app in the logs and terminate it: + +.. code-block:: console + + $ kill -TERM 43597 + +The logs show that Supervisor restarted this instance: + +.. code-block:: console + + INFO exited: websockets-test_00 (exit status 0; expected) + INFO spawned: 'websockets-test_00' with pid 43629 + INFO success: websockets-test_00 entered RUNNING state, process has stayed up for > than 1 seconds (startsecs) + +Now let's check what happens when we shut down Supervisor, but first let's +establish a connection and leave it open: + +.. code-block:: console + + $ python -m websockets ws://localhost:8080/ + Connected to ws://localhost:8080/. + > + +Look at the pid of supervisord itself in the logs and terminate it: + +.. code-block:: console + + $ kill -TERM 43596 + +The logs show that Supervisor terminated all instances of the app before +exiting: + +.. code-block:: console + + WARN received SIGTERM indicating exit request + INFO waiting for websockets-test_00, websockets-test_01, websockets-test_02, websockets-test_03 to die + INFO stopped: websockets-test_02 (exit status 0) + INFO stopped: websockets-test_03 (exit status 0) + INFO stopped: websockets-test_01 (exit status 0) + INFO stopped: websockets-test_00 (exit status 0) + +And you can see that the connection to the app was closed gracefully: + +.. code-block:: console + + $ python -m websockets ws://localhost:8080/ + Connected to ws://localhost:8080/. + Connection closed: 1001 (going away). + +In this example, we've been sharing the same virtualenv for supervisor and +websockets. + +In a real deployment, you would likely: + +* Install Supervisor with the package manager of the OS. +* Create a virtualenv dedicated to your application. +* Add ``environment=PATH="path/to/your/virtualenv/bin"`` in the Supervisor + configuration. Then ``python app.py`` runs in that virtualenv. + diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/index.rst b/tests/wpt/tests/tools/third_party/websockets/docs/index.rst new file mode 100644 index 00000000000..d9737db12a6 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/index.rst @@ -0,0 +1,75 @@ +websockets +========== + +|licence| |version| |pyversions| |tests| |docs| |openssf| + +.. |licence| image:: https://img.shields.io/pypi/l/websockets.svg + :target: https://pypi.python.org/pypi/websockets + +.. |version| image:: https://img.shields.io/pypi/v/websockets.svg + :target: https://pypi.python.org/pypi/websockets + +.. |pyversions| image:: https://img.shields.io/pypi/pyversions/websockets.svg + :target: https://pypi.python.org/pypi/websockets + +.. |tests| image:: https://img.shields.io/github/checks-status/python-websockets/websockets/main?label=tests + :target: https://github.com/python-websockets/websockets/actions/workflows/tests.yml + +.. |docs| image:: https://img.shields.io/readthedocs/websockets.svg + :target: https://websockets.readthedocs.io/ + +.. |openssf| image:: https://bestpractices.coreinfrastructure.org/projects/6475/badge + :target: https://bestpractices.coreinfrastructure.org/projects/6475 + +websockets is a library for building WebSocket_ servers and clients in Python +with a focus on correctness, simplicity, robustness, and performance. + +.. _WebSocket: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API + +It supports several network I/O and control flow paradigms: + +1. The default implementation builds upon :mod:`asyncio`, Python's standard + asynchronous I/O framework. It provides an elegant coroutine-based API. It's + ideal for servers that handle many clients concurrently. +2. The :mod:`threading` implementation is a good alternative for clients, + especially if you aren't familiar with :mod:`asyncio`. It may also be used + for servers that don't need to serve many clients. +3. The `Sans-I/O`_ implementation is designed for integrating in third-party + libraries, typically application servers, in addition being used internally + by websockets. + +.. _Sans-I/O: https://sans-io.readthedocs.io/ + +Here's an echo server with the :mod:`asyncio` API: + +.. literalinclude:: ../example/echo.py + +Here's how a client sends and receives messages with the :mod:`threading` API: + +.. literalinclude:: ../example/hello.py + +Don't worry about the opening and closing handshakes, pings and pongs, or any +other behavior described in the WebSocket specification. websockets takes care +of this under the hood so you can focus on your application! + +Also, websockets provides an interactive client: + +.. code-block:: console + + $ python -m websockets ws://localhost:8765/ + Connected to ws://localhost:8765/. + > Hello world! + < Hello world! + Connection closed: 1000 (OK). + +Do you like it? :doc:`Let's dive in! <intro/index>` + +.. toctree:: + :hidden: + + intro/index + howto/index + faq/index + reference/index + topics/index + project/index diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/intro/index.rst b/tests/wpt/tests/tools/third_party/websockets/docs/intro/index.rst new file mode 100644 index 00000000000..095262a2073 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/intro/index.rst @@ -0,0 +1,46 @@ +Getting started +=============== + +.. currentmodule:: websockets + +Requirements +------------ + +websockets requires Python ≥ 3.8. + +.. admonition:: Use the most recent Python release + :class: tip + + For each minor version (3.x), only the latest bugfix or security release + (3.x.y) is officially supported. + +It doesn't have any dependencies. + +.. _install: + +Installation +------------ + +Install websockets with: + +.. code-block:: console + + $ pip install websockets + +Wheels are available for all platforms. + +Tutorial +-------- + +Learn how to build an real-time web application with websockets. + +.. toctree:: + + tutorial1 + tutorial2 + tutorial3 + +In a hurry? +----------- + +Look at the :doc:`quick start guide <../howto/quickstart>`. diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/intro/tutorial1.rst b/tests/wpt/tests/tools/third_party/websockets/docs/intro/tutorial1.rst new file mode 100644 index 00000000000..ff85003b58a --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/intro/tutorial1.rst @@ -0,0 +1,591 @@ +Part 1 - Send & receive +======================= + +.. currentmodule:: websockets + +In this tutorial, you're going to build a web-based `Connect Four`_ game. + +.. _Connect Four: https://en.wikipedia.org/wiki/Connect_Four + +The web removes the constraint of being in the same room for playing a game. +Two players can connect over of the Internet, regardless of where they are, +and play in their browsers. + +When a player makes a move, it should be reflected immediately on both sides. +This is difficult to implement over HTTP due to the request-response style of +the protocol. + +Indeed, there is no good way to be notified when the other player makes a +move. Workarounds such as polling or long-polling introduce significant +overhead. + +Enter `WebSocket <websocket>`_. + +The WebSocket protocol provides two-way communication between a browser and a +server over a persistent connection. That's exactly what you need to exchange +moves between players, via a server. + +.. admonition:: This is the first part of the tutorial. + + * In this :doc:`first part <tutorial1>`, you will create a server and + connect one browser; you can play if you share the same browser. + * In the :doc:`second part <tutorial2>`, you will connect a second + browser; you can play from different browsers on a local network. + * In the :doc:`third part <tutorial3>`, you will deploy the game to the + web; you can play from any browser connected to the Internet. + +Prerequisites +------------- + +This tutorial assumes basic knowledge of Python and JavaScript. + +If you're comfortable with :doc:`virtual environments <python:tutorial/venv>`, +you can use one for this tutorial. Else, don't worry: websockets doesn't have +any dependencies; it shouldn't create trouble in the default environment. + +If you haven't installed websockets yet, do it now: + +.. code-block:: console + + $ pip install websockets + +Confirm that websockets is installed: + +.. code-block:: console + + $ python -m websockets --version + +.. admonition:: This tutorial is written for websockets |release|. + :class: tip + + If you installed another version, you should switch to the corresponding + version of the documentation. + +Download the starter kit +------------------------ + +Create a directory and download these three files: +:download:`connect4.js <../../example/tutorial/start/connect4.js>`, +:download:`connect4.css <../../example/tutorial/start/connect4.css>`, +and :download:`connect4.py <../../example/tutorial/start/connect4.py>`. + +The JavaScript module, along with the CSS file, provides a web-based user +interface. Here's its API. + +.. js:module:: connect4 + +.. js:data:: PLAYER1 + + Color of the first player. + +.. js:data:: PLAYER2 + + Color of the second player. + +.. js:function:: createBoard(board) + + Draw a board. + + :param board: DOM element containing the board; must be initially empty. + +.. js:function:: playMove(board, player, column, row) + + Play a move. + + :param board: DOM element containing the board. + :param player: :js:data:`PLAYER1` or :js:data:`PLAYER2`. + :param column: between ``0`` and ``6``. + :param row: between ``0`` and ``5``. + +The Python module provides a class to record moves and tell when a player +wins. Here's its API. + +.. module:: connect4 + +.. data:: PLAYER1 + :value: "red" + + Color of the first player. + +.. data:: PLAYER2 + :value: "yellow" + + Color of the second player. + +.. class:: Connect4 + + A Connect Four game. + + .. method:: play(player, column) + + Play a move. + + :param player: :data:`~connect4.PLAYER1` or :data:`~connect4.PLAYER2`. + :param column: between ``0`` and ``6``. + :returns: Row where the checker lands, between ``0`` and ``5``. + :raises RuntimeError: if the move is illegal. + + .. attribute:: moves + + List of moves played during this game, as ``(player, column, row)`` + tuples. + + .. attribute:: winner + + :data:`~connect4.PLAYER1` or :data:`~connect4.PLAYER2` if they + won; :obj:`None` if the game is still ongoing. + +.. currentmodule:: websockets + +Bootstrap the web UI +-------------------- + +Create an ``index.html`` file next to ``connect4.js`` and ``connect4.css`` +with this content: + +.. literalinclude:: ../../example/tutorial/step1/index.html + :language: html + +This HTML page contains an empty ``<div>`` element where you will draw the +Connect Four board. It loads a ``main.js`` script where you will write all +your JavaScript code. + +Create a ``main.js`` file next to ``index.html``. In this script, when the +page loads, draw the board: + +.. code-block:: javascript + + import { createBoard, playMove } from "./connect4.js"; + + window.addEventListener("DOMContentLoaded", () => { + // Initialize the UI. + const board = document.querySelector(".board"); + createBoard(board); + }); + +Open a shell, navigate to the directory containing these files, and start an +HTTP server: + +.. code-block:: console + + $ python -m http.server + +Open http://localhost:8000/ in a web browser. The page displays an empty board +with seven columns and six rows. You will play moves in this board later. + +Bootstrap the server +-------------------- + +Create an ``app.py`` file next to ``connect4.py`` with this content: + +.. code-block:: python + + #!/usr/bin/env python + + import asyncio + + import websockets + + + async def handler(websocket): + while True: + message = await websocket.recv() + print(message) + + + async def main(): + async with websockets.serve(handler, "", 8001): + await asyncio.Future() # run forever + + + if __name__ == "__main__": + asyncio.run(main()) + +The entry point of this program is ``asyncio.run(main())``. It creates an +asyncio event loop, runs the ``main()`` coroutine, and shuts down the loop. + +The ``main()`` coroutine calls :func:`~server.serve` to start a websockets +server. :func:`~server.serve` takes three positional arguments: + +* ``handler`` is a coroutine that manages a connection. When a client + connects, websockets calls ``handler`` with the connection in argument. + When ``handler`` terminates, websockets closes the connection. +* The second argument defines the network interfaces where the server can be + reached. Here, the server listens on all interfaces, so that other devices + on the same local network can connect. +* The third argument is the port on which the server listens. + +Invoking :func:`~server.serve` as an asynchronous context manager, in an +``async with`` block, ensures that the server shuts down properly when +terminating the program. + +For each connection, the ``handler()`` coroutine runs an infinite loop that +receives messages from the browser and prints them. + +Open a shell, navigate to the directory containing ``app.py``, and start the +server: + +.. code-block:: console + + $ python app.py + +This doesn't display anything. Hopefully the WebSocket server is running. +Let's make sure that it works. You cannot test the WebSocket server with a +web browser like you tested the HTTP server. However, you can test it with +websockets' interactive client. + +Open another shell and run this command: + +.. code-block:: console + + $ python -m websockets ws://localhost:8001/ + +You get a prompt. Type a message and press "Enter". Switch to the shell where +the server is running and check that the server received the message. Good! + +Exit the interactive client with Ctrl-C or Ctrl-D. + +Now, if you look at the console where you started the server, you can see the +stack trace of an exception: + +.. code-block:: pytb + + connection handler failed + Traceback (most recent call last): + ... + File "app.py", line 22, in handler + message = await websocket.recv() + ... + websockets.exceptions.ConnectionClosedOK: received 1000 (OK); then sent 1000 (OK) + +Indeed, the server was waiting for the next message +with :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` when the client +disconnected. When this happens, websockets raises +a :exc:`~exceptions.ConnectionClosedOK` exception to let you know that you +won't receive another message on this connection. + +This exception creates noise in the server logs, making it more difficult to +spot real errors when you add functionality to the server. Catch it in the +``handler()`` coroutine: + +.. code-block:: python + + async def handler(websocket): + while True: + try: + message = await websocket.recv() + except websockets.ConnectionClosedOK: + break + print(message) + +Stop the server with Ctrl-C and start it again: + +.. code-block:: console + + $ python app.py + +.. admonition:: You must restart the WebSocket server when you make changes. + :class: tip + + The WebSocket server loads the Python code in ``app.py`` then serves every + WebSocket request with this version of the code. As a consequence, + changes to ``app.py`` aren't visible until you restart the server. + + This is unlike the HTTP server that you started earlier with ``python -m + http.server``. For every request, this HTTP server reads the target file + and sends it. That's why changes are immediately visible. + + It is possible to :doc:`restart the WebSocket server automatically + <../howto/autoreload>` but this isn't necessary for this tutorial. + +Try connecting and disconnecting the interactive client again. +The :exc:`~exceptions.ConnectionClosedOK` exception doesn't appear anymore. + +This pattern is so common that websockets provides a shortcut for iterating +over messages received on the connection until the client disconnects: + +.. code-block:: python + + async def handler(websocket): + async for message in websocket: + print(message) + +Restart the server and check with the interactive client that its behavior +didn't change. + +At this point, you bootstrapped a web application and a WebSocket server. +Let's connect them. + +Transmit from browser to server +------------------------------- + +In JavaScript, you open a WebSocket connection as follows: + +.. code-block:: javascript + + const websocket = new WebSocket("ws://localhost:8001/"); + +Before you exchange messages with the server, you need to decide their format. +There is no universal convention for this. + +Let's use JSON objects with a ``type`` key identifying the type of the event +and the rest of the object containing properties of the event. + +Here's an event describing a move in the middle slot of the board: + +.. code-block:: javascript + + const event = {type: "play", column: 3}; + +Here's how to serialize this event to JSON and send it to the server: + +.. code-block:: javascript + + websocket.send(JSON.stringify(event)); + +Now you have all the building blocks to send moves to the server. + +Add this function to ``main.js``: + +.. literalinclude:: ../../example/tutorial/step1/main.js + :language: js + :start-at: function sendMoves + :end-before: window.addEventListener + +``sendMoves()`` registers a listener for ``click`` events on the board. The +listener figures out which column was clicked, builds a event of type +``"play"``, serializes it, and sends it to the server. + +Modify the initialization to open the WebSocket connection and call the +``sendMoves()`` function: + +.. code-block:: javascript + + window.addEventListener("DOMContentLoaded", () => { + // Initialize the UI. + const board = document.querySelector(".board"); + createBoard(board); + // Open the WebSocket connection and register event handlers. + const websocket = new WebSocket("ws://localhost:8001/"); + sendMoves(board, websocket); + }); + +Check that the HTTP server and the WebSocket server are still running. If you +stopped them, here are the commands to start them again: + +.. code-block:: console + + $ python -m http.server + +.. code-block:: console + + $ python app.py + +Refresh http://localhost:8000/ in your web browser. Click various columns in +the board. The server receives messages with the expected column number. + +There isn't any feedback in the board because you haven't implemented that +yet. Let's do it. + +Transmit from server to browser +------------------------------- + +In JavaScript, you receive WebSocket messages by listening to ``message`` +events. Here's how to receive a message from the server and deserialize it +from JSON: + +.. code-block:: javascript + + websocket.addEventListener("message", ({ data }) => { + const event = JSON.parse(data); + // do something with event + }); + +You're going to need three types of messages from the server to the browser: + +.. code-block:: javascript + + {type: "play", player: "red", column: 3, row: 0} + {type: "win", player: "red"} + {type: "error", message: "This slot is full."} + +The JavaScript code receiving these messages will dispatch events depending on +their type and take appropriate action. For example, it will react to an +event of type ``"play"`` by displaying the move on the board with +the :js:func:`~connect4.playMove` function. + +Add this function to ``main.js``: + +.. literalinclude:: ../../example/tutorial/step1/main.js + :language: js + :start-at: function showMessage + :end-before: function sendMoves + +.. admonition:: Why does ``showMessage`` use ``window.setTimeout``? + :class: hint + + When :js:func:`playMove` modifies the state of the board, the browser + renders changes asynchronously. Conversely, ``window.alert()`` runs + synchronously and blocks rendering while the alert is visible. + + If you called ``window.alert()`` immediately after :js:func:`playMove`, + the browser could display the alert before rendering the move. You could + get a "Player red wins!" alert without seeing red's last move. + + We're using ``window.alert()`` for simplicity in this tutorial. A real + application would display these messages in the user interface instead. + It wouldn't be vulnerable to this problem. + +Modify the initialization to call the ``receiveMoves()`` function: + +.. literalinclude:: ../../example/tutorial/step1/main.js + :language: js + :start-at: window.addEventListener + +At this point, the user interface should receive events properly. Let's test +it by modifying the server to send some events. + +Sending an event from Python is quite similar to JavaScript: + +.. code-block:: python + + event = {"type": "play", "player": "red", "column": 3, "row": 0} + await websocket.send(json.dumps(event)) + +.. admonition:: Don't forget to serialize the event with :func:`json.dumps`. + :class: tip + + Else, websockets raises ``TypeError: data is a dict-like object``. + +Modify the ``handler()`` coroutine in ``app.py`` as follows: + +.. code-block:: python + + import json + + from connect4 import PLAYER1, PLAYER2 + + async def handler(websocket): + for player, column, row in [ + (PLAYER1, 3, 0), + (PLAYER2, 3, 1), + (PLAYER1, 4, 0), + (PLAYER2, 4, 1), + (PLAYER1, 2, 0), + (PLAYER2, 1, 0), + (PLAYER1, 5, 0), + ]: + event = { + "type": "play", + "player": player, + "column": column, + "row": row, + } + await websocket.send(json.dumps(event)) + await asyncio.sleep(0.5) + event = { + "type": "win", + "player": PLAYER1, + } + await websocket.send(json.dumps(event)) + +Restart the WebSocket server and refresh http://localhost:8000/ in your web +browser. Seven moves appear at 0.5 second intervals. Then an alert announces +the winner. + +Good! Now you know how to communicate both ways. + +Once you plug the game engine to process moves, you will have a fully +functional game. + +Add the game logic +------------------ + +In the ``handler()`` coroutine, you're going to initialize a game: + +.. code-block:: python + + from connect4 import Connect4 + + async def handler(websocket): + # Initialize a Connect Four game. + game = Connect4() + + ... + +Then, you're going to iterate over incoming messages and take these steps: + +* parse an event of type ``"play"``, the only type of event that the user + interface sends; +* play the move in the board with the :meth:`~connect4.Connect4.play` method, + alternating between the two players; +* if :meth:`~connect4.Connect4.play` raises :exc:`RuntimeError` because the + move is illegal, send an event of type ``"error"``; +* else, send an event of type ``"play"`` to tell the user interface where the + checker lands; +* if the move won the game, send an event of type ``"win"``. + +Try to implement this by yourself! + +Keep in mind that you must restart the WebSocket server and reload the page in +the browser when you make changes. + +When it works, you can play the game from a single browser, with players +taking alternate turns. + +.. admonition:: Enable debug logs to see all messages sent and received. + :class: tip + + Here's how to enable debug logs: + + .. code-block:: python + + import logging + + logging.basicConfig(format="%(message)s", level=logging.DEBUG) + +If you're stuck, a solution is available at the bottom of this document. + +Summary +------- + +In this first part of the tutorial, you learned how to: + +* build and run a WebSocket server in Python with :func:`~server.serve`; +* receive a message in a connection handler + with :meth:`~server.WebSocketServerProtocol.recv`; +* send a message in a connection handler + with :meth:`~server.WebSocketServerProtocol.send`; +* iterate over incoming messages with ``async for + message in websocket: ...``; +* open a WebSocket connection in JavaScript with the ``WebSocket`` API; +* send messages in a browser with ``WebSocket.send()``; +* receive messages in a browser by listening to ``message`` events; +* design a set of events to be exchanged between the browser and the server. + +You can now play a Connect Four game in a browser, communicating over a +WebSocket connection with a server where the game logic resides! + +However, the two players share a browser, so the constraint of being in the +same room still applies. + +Move on to the :doc:`second part <tutorial2>` of the tutorial to break this +constraint and play from separate browsers. + +Solution +-------- + +.. literalinclude:: ../../example/tutorial/step1/app.py + :caption: app.py + :language: python + :linenos: + +.. literalinclude:: ../../example/tutorial/step1/index.html + :caption: index.html + :language: html + :linenos: + +.. literalinclude:: ../../example/tutorial/step1/main.js + :caption: main.js + :language: js + :linenos: diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/intro/tutorial2.rst b/tests/wpt/tests/tools/third_party/websockets/docs/intro/tutorial2.rst new file mode 100644 index 00000000000..5ac4ae9dd55 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/intro/tutorial2.rst @@ -0,0 +1,565 @@ +Part 2 - Route & broadcast +========================== + +.. currentmodule:: websockets + +.. admonition:: This is the second part of the tutorial. + + * In the :doc:`first part <tutorial1>`, you created a server and + connected one browser; you could play if you shared the same browser. + * In this :doc:`second part <tutorial2>`, you will connect a second + browser; you can play from different browsers on a local network. + * In the :doc:`third part <tutorial3>`, you will deploy the game to the + web; you can play from any browser connected to the Internet. + +In the first part of the tutorial, you opened a WebSocket connection from a +browser to a server and exchanged events to play moves. The state of the game +was stored in an instance of the :class:`~connect4.Connect4` class, +referenced as a local variable in the connection handler coroutine. + +Now you want to open two WebSocket connections from two separate browsers, one +for each player, to the same server in order to play the same game. This +requires moving the state of the game to a place where both connections can +access it. + +Share game state +---------------- + +As long as you're running a single server process, you can share state by +storing it in a global variable. + +.. admonition:: What if you need to scale to multiple server processes? + :class: hint + + In that case, you must design a way for the process that handles a given + connection to be aware of relevant events for that client. This is often + achieved with a publish / subscribe mechanism. + +How can you make two connection handlers agree on which game they're playing? +When the first player starts a game, you give it an identifier. Then, you +communicate the identifier to the second player. When the second player joins +the game, you look it up with the identifier. + +In addition to the game itself, you need to keep track of the WebSocket +connections of the two players. Since both players receive the same events, +you don't need to treat the two connections differently; you can store both +in the same set. + +Let's sketch this in code. + +A module-level :class:`dict` enables lookups by identifier: + +.. code-block:: python + + JOIN = {} + +When the first player starts the game, initialize and store it: + +.. code-block:: python + + import secrets + + async def handler(websocket): + ... + + # Initialize a Connect Four game, the set of WebSocket connections + # receiving moves from this game, and secret access token. + game = Connect4() + connected = {websocket} + + join_key = secrets.token_urlsafe(12) + JOIN[join_key] = game, connected + + try: + + ... + + finally: + del JOIN[join_key] + +When the second player joins the game, look it up: + +.. code-block:: python + + async def handler(websocket): + ... + + join_key = ... # TODO + + # Find the Connect Four game. + game, connected = JOIN[join_key] + + # Register to receive moves from this game. + connected.add(websocket) + try: + + ... + + finally: + connected.remove(websocket) + +Notice how we're carefully cleaning up global state with ``try: ... +finally: ...`` blocks. Else, we could leave references to games or +connections in global state, which would cause a memory leak. + +In both connection handlers, you have a ``game`` pointing to the same +:class:`~connect4.Connect4` instance, so you can interact with the game, +and a ``connected`` set of connections, so you can send game events to +both players as follows: + +.. code-block:: python + + async def handler(websocket): + + ... + + for connection in connected: + await connection.send(json.dumps(event)) + + ... + +Perhaps you spotted a major piece missing from the puzzle. How does the second +player obtain ``join_key``? Let's design new events to carry this information. + +To start a game, the first player sends an ``"init"`` event: + +.. code-block:: javascript + + {type: "init"} + +The connection handler for the first player creates a game as shown above and +responds with: + +.. code-block:: javascript + + {type: "init", join: "<join_key>"} + +With this information, the user interface of the first player can create a +link to ``http://localhost:8000/?join=<join_key>``. For the sake of simplicity, +we will assume that the first player shares this link with the second player +outside of the application, for example via an instant messaging service. + +To join the game, the second player sends a different ``"init"`` event: + +.. code-block:: javascript + + {type: "init", join: "<join_key>"} + +The connection handler for the second player can look up the game with the +join key as shown above. There is no need to respond. + +Let's dive into the details of implementing this design. + +Start a game +------------ + +We'll start with the initialization sequence for the first player. + +In ``main.js``, define a function to send an initialization event when the +WebSocket connection is established, which triggers an ``open`` event: + +.. code-block:: javascript + + function initGame(websocket) { + websocket.addEventListener("open", () => { + // Send an "init" event for the first player. + const event = { type: "init" }; + websocket.send(JSON.stringify(event)); + }); + } + +Update the initialization sequence to call ``initGame()``: + +.. literalinclude:: ../../example/tutorial/step2/main.js + :language: js + :start-at: window.addEventListener + +In ``app.py``, define a new ``handler`` coroutine — keep a copy of the +previous one to reuse it later: + +.. code-block:: python + + import secrets + + + JOIN = {} + + + async def start(websocket): + # Initialize a Connect Four game, the set of WebSocket connections + # receiving moves from this game, and secret access token. + game = Connect4() + connected = {websocket} + + join_key = secrets.token_urlsafe(12) + JOIN[join_key] = game, connected + + try: + # Send the secret access token to the browser of the first player, + # where it'll be used for building a "join" link. + event = { + "type": "init", + "join": join_key, + } + await websocket.send(json.dumps(event)) + + # Temporary - for testing. + print("first player started game", id(game)) + async for message in websocket: + print("first player sent", message) + + finally: + del JOIN[join_key] + + + async def handler(websocket): + # Receive and parse the "init" event from the UI. + message = await websocket.recv() + event = json.loads(message) + assert event["type"] == "init" + + # First player starts a new game. + await start(websocket) + +In ``index.html``, add an ``<a>`` element to display the link to share with +the other player. + +.. code-block:: html + + <body> + <div class="actions"> + <a class="action join" href="">Join</a> + </div> + <!-- ... --> + </body> + +In ``main.js``, modify ``receiveMoves()`` to handle the ``"init"`` message and +set the target of that link: + +.. code-block:: javascript + + switch (event.type) { + case "init": + // Create link for inviting the second player. + document.querySelector(".join").href = "?join=" + event.join; + break; + // ... + } + +Restart the WebSocket server and reload http://localhost:8000/ in the browser. +There's a link labeled JOIN below the board with a target that looks like +http://localhost:8000/?join=95ftAaU5DJVP1zvb. + +The server logs say ``first player started game ...``. If you click the board, +you see ``"play"`` events. There is no feedback in the UI, though, because +you haven't restored the game logic yet. + +Before we get there, let's handle links with a ``join`` query parameter. + +Join a game +----------- + +We'll now update the initialization sequence to account for the second +player. + +In ``main.js``, update ``initGame()`` to send the join key in the ``"init"`` +message when it's in the URL: + +.. code-block:: javascript + + function initGame(websocket) { + websocket.addEventListener("open", () => { + // Send an "init" event according to who is connecting. + const params = new URLSearchParams(window.location.search); + let event = { type: "init" }; + if (params.has("join")) { + // Second player joins an existing game. + event.join = params.get("join"); + } else { + // First player starts a new game. + } + websocket.send(JSON.stringify(event)); + }); + } + +In ``app.py``, update the ``handler`` coroutine to look for the join key in +the ``"init"`` message, then load that game: + +.. code-block:: python + + async def error(websocket, message): + event = { + "type": "error", + "message": message, + } + await websocket.send(json.dumps(event)) + + + async def join(websocket, join_key): + # Find the Connect Four game. + try: + game, connected = JOIN[join_key] + except KeyError: + await error(websocket, "Game not found.") + return + + # Register to receive moves from this game. + connected.add(websocket) + try: + + # Temporary - for testing. + print("second player joined game", id(game)) + async for message in websocket: + print("second player sent", message) + + finally: + connected.remove(websocket) + + + async def handler(websocket): + # Receive and parse the "init" event from the UI. + message = await websocket.recv() + event = json.loads(message) + assert event["type"] == "init" + + if "join" in event: + # Second player joins an existing game. + await join(websocket, event["join"]) + else: + # First player starts a new game. + await start(websocket) + +Restart the WebSocket server and reload http://localhost:8000/ in the browser. + +Copy the link labeled JOIN and open it in another browser. You may also open +it in another tab or another window of the same browser; however, that makes +it a bit tricky to remember which one is the first or second player. + +.. admonition:: You must start a new game when you restart the server. + :class: tip + + Since games are stored in the memory of the Python process, they're lost + when you stop the server. + + Whenever you make changes to ``app.py``, you must restart the server, + create a new game in a browser, and join it in another browser. + +The server logs say ``first player started game ...`` and ``second player +joined game ...``. The numbers match, proving that the ``game`` local +variable in both connection handlers points to same object in the memory of +the Python process. + +Click the board in either browser. The server receives ``"play"`` events from +the corresponding player. + +In the initialization sequence, you're routing connections to ``start()`` or +``join()`` depending on the first message received by the server. This is a +common pattern in servers that handle different clients. + +.. admonition:: Why not use different URIs for ``start()`` and ``join()``? + :class: hint + + Instead of sending an initialization event, you could encode the join key + in the WebSocket URI e.g. ``ws://localhost:8001/join/<join_key>``. The + WebSocket server would parse ``websocket.path`` and route the connection, + similar to how HTTP servers route requests. + + When you need to send sensitive data like authentication credentials to + the server, sending it an event is considered more secure than encoding + it in the URI because URIs end up in logs. + + For the purposes of this tutorial, both approaches are equivalent because + the join key comes from an HTTP URL. There isn't much at risk anyway! + +Now you can restore the logic for playing moves and you'll have a fully +functional two-player game. + +Add the game logic +------------------ + +Once the initialization is done, the game is symmetrical, so you can write a +single coroutine to process the moves of both players: + +.. code-block:: python + + async def play(websocket, game, player, connected): + ... + +With such a coroutine, you can replace the temporary code for testing in +``start()`` by: + +.. code-block:: python + + await play(websocket, game, PLAYER1, connected) + +and in ``join()`` by: + +.. code-block:: python + + await play(websocket, game, PLAYER2, connected) + +The ``play()`` coroutine will reuse much of the code you wrote in the first +part of the tutorial. + +Try to implement this by yourself! + +Keep in mind that you must restart the WebSocket server, reload the page to +start a new game with the first player, copy the JOIN link, and join the game +with the second player when you make changes. + +When ``play()`` works, you can play the game from two separate browsers, +possibly running on separate computers on the same local network. + +A complete solution is available at the bottom of this document. + +Watch a game +------------ + +Let's add one more feature: allow spectators to watch the game. + +The process for inviting a spectator can be the same as for inviting the +second player. You will have to duplicate all the initialization logic: + +- declare a ``WATCH`` global variable similar to ``JOIN``; +- generate a watch key when creating a game; it must be different from the + join key, or else a spectator could hijack a game by tweaking the URL; +- include the watch key in the ``"init"`` event sent to the first player; +- generate a WATCH link in the UI with a ``watch`` query parameter; +- update the ``initGame()`` function to handle such links; +- update the ``handler()`` coroutine to invoke a ``watch()`` coroutine for + spectators; +- prevent ``sendMoves()`` from sending ``"play"`` events for spectators. + +Once the initialization sequence is done, watching a game is as simple as +registering the WebSocket connection in the ``connected`` set in order to +receive game events and doing nothing until the spectator disconnects. You +can wait for a connection to terminate with +:meth:`~legacy.protocol.WebSocketCommonProtocol.wait_closed`: + +.. code-block:: python + + async def watch(websocket, watch_key): + + ... + + connected.add(websocket) + try: + await websocket.wait_closed() + finally: + connected.remove(websocket) + +The connection can terminate because the ``receiveMoves()`` function closed it +explicitly after receiving a ``"win"`` event, because the spectator closed +their browser, or because the network failed. + +Again, try to implement this by yourself. + +When ``watch()`` works, you can invite spectators to watch the game from other +browsers, as long as they're on the same local network. + +As a further improvement, you may support adding spectators while a game is +already in progress. This requires replaying moves that were played before +the spectator was added to the ``connected`` set. Past moves are available in +the :attr:`~connect4.Connect4.moves` attribute of the game. + +This feature is included in the solution proposed below. + +Broadcast +--------- + +When you need to send a message to the two players and to all spectators, +you're using this pattern: + +.. code-block:: python + + async def handler(websocket): + + ... + + for connection in connected: + await connection.send(json.dumps(event)) + + ... + +Since this is a very common pattern in WebSocket servers, websockets provides +the :func:`broadcast` helper for this purpose: + +.. code-block:: python + + async def handler(websocket): + + ... + + websockets.broadcast(connected, json.dumps(event)) + + ... + +Calling :func:`broadcast` once is more efficient than +calling :meth:`~legacy.protocol.WebSocketCommonProtocol.send` in a loop. + +However, there's a subtle difference in behavior. Did you notice that there's +no ``await`` in the second version? Indeed, :func:`broadcast` is a function, +not a coroutine like :meth:`~legacy.protocol.WebSocketCommonProtocol.send` +or :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`. + +It's quite obvious why :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` +is a coroutine. When you want to receive the next message, you have to wait +until the client sends it and the network transmits it. + +It's less obvious why :meth:`~legacy.protocol.WebSocketCommonProtocol.send` is +a coroutine. If you send many messages or large messages, you could write +data faster than the network can transmit it or the client can read it. Then, +outgoing data will pile up in buffers, which will consume memory and may +crash your application. + +To avoid this problem, :meth:`~legacy.protocol.WebSocketCommonProtocol.send` +waits until the write buffer drains. By slowing down the application as +necessary, this ensures that the server doesn't send data too quickly. This +is called backpressure and it's useful for building robust systems. + +That said, when you're sending the same messages to many clients in a loop, +applying backpressure in this way can become counterproductive. When you're +broadcasting, you don't want to slow down everyone to the pace of the slowest +clients; you want to drop clients that cannot keep up with the data stream. +That's why :func:`broadcast` doesn't wait until write buffers drain. + +For our Connect Four game, there's no difference in practice: the total amount +of data sent on a connection for a game of Connect Four is less than 64 KB, +so the write buffer never fills up and backpressure never kicks in anyway. + +Summary +------- + +In this second part of the tutorial, you learned how to: + +* configure a connection by exchanging initialization messages; +* keep track of connections within a single server process; +* wait until a client disconnects in a connection handler; +* broadcast a message to many connections efficiently. + +You can now play a Connect Four game from separate browser, communicating over +WebSocket connections with a server that synchronizes the game logic! + +However, the two players have to be on the same local network as the server, +so the constraint of being in the same place still mostly applies. + +Head over to the :doc:`third part <tutorial3>` of the tutorial to deploy the +game to the web and remove this constraint. + +Solution +-------- + +.. literalinclude:: ../../example/tutorial/step2/app.py + :caption: app.py + :language: python + :linenos: + +.. literalinclude:: ../../example/tutorial/step2/index.html + :caption: index.html + :language: html + :linenos: + +.. literalinclude:: ../../example/tutorial/step2/main.js + :caption: main.js + :language: js + :linenos: diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/intro/tutorial3.rst b/tests/wpt/tests/tools/third_party/websockets/docs/intro/tutorial3.rst new file mode 100644 index 00000000000..6fdec113b2a --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/intro/tutorial3.rst @@ -0,0 +1,290 @@ +Part 3 - Deploy to the web +========================== + +.. currentmodule:: websockets + +.. admonition:: This is the third part of the tutorial. + + * In the :doc:`first part <tutorial1>`, you created a server and + connected one browser; you could play if you shared the same browser. + * In this :doc:`second part <tutorial2>`, you connected a second browser; + you could play from different browsers on a local network. + * In this :doc:`third part <tutorial3>`, you will deploy the game to the + web; you can play from any browser connected to the Internet. + +In the first and second parts of the tutorial, for local development, you ran +an HTTP server on ``http://localhost:8000/`` with: + +.. code-block:: console + + $ python -m http.server + +and a WebSocket server on ``ws://localhost:8001/`` with: + +.. code-block:: console + + $ python app.py + +Now you want to deploy these servers on the Internet. There's a vast range of +hosting providers to choose from. For the sake of simplicity, we'll rely on: + +* GitHub Pages for the HTTP server; +* Heroku for the WebSocket server. + +Commit project to git +--------------------- + +Perhaps you committed your work to git while you were progressing through the +tutorial. If you didn't, now is a good time, because GitHub and Heroku offer +git-based deployment workflows. + +Initialize a git repository: + +.. code-block:: console + + $ git init -b main + Initialized empty Git repository in websockets-tutorial/.git/ + $ git commit --allow-empty -m "Initial commit." + [main (root-commit) ...] Initial commit. + +Add all files and commit: + +.. code-block:: console + + $ git add . + $ git commit -m "Initial implementation of Connect Four game." + [main ...] Initial implementation of Connect Four game. + 6 files changed, 500 insertions(+) + create mode 100644 app.py + create mode 100644 connect4.css + create mode 100644 connect4.js + create mode 100644 connect4.py + create mode 100644 index.html + create mode 100644 main.js + +Prepare the WebSocket server +---------------------------- + +Before you deploy the server, you must adapt it to meet requirements of +Heroku's runtime. This involves two small changes: + +1. Heroku expects the server to `listen on a specific port`_, provided in the + ``$PORT`` environment variable. + +2. Heroku sends a ``SIGTERM`` signal when `shutting down a dyno`_, which + should trigger a clean exit. + +.. _listen on a specific port: https://devcenter.heroku.com/articles/preparing-a-codebase-for-heroku-deployment#4-listen-on-the-correct-port + +.. _shutting down a dyno: https://devcenter.heroku.com/articles/dynos#shutdown + +Adapt the ``main()`` coroutine accordingly: + +.. code-block:: python + + import os + import signal + +.. literalinclude:: ../../example/tutorial/step3/app.py + :pyobject: main + +To catch the ``SIGTERM`` signal, ``main()`` creates a :class:`~asyncio.Future` +called ``stop`` and registers a signal handler that sets the result of this +future. The value of the future doesn't matter; it's only for waiting for +``SIGTERM``. + +Then, by using :func:`~server.serve` as a context manager and exiting the +context when ``stop`` has a result, ``main()`` ensures that the server closes +connections cleanly and exits on ``SIGTERM``. + +The app is now fully compatible with Heroku. + +Deploy the WebSocket server +--------------------------- + +Create a ``requirements.txt`` file with this content to install ``websockets`` +when building the image: + +.. literalinclude:: ../../example/tutorial/step3/requirements.txt + :language: text + +.. admonition:: Heroku treats ``requirements.txt`` as a signal to `detect a Python app`_. + :class: tip + + That's why you don't need to declare that you need a Python runtime. + +.. _detect a Python app: https://devcenter.heroku.com/articles/python-support#recognizing-a-python-app + +Create a ``Procfile`` file with this content to configure the command for +running the server: + +.. literalinclude:: ../../example/tutorial/step3/Procfile + :language: text + +Commit your changes: + +.. code-block:: console + + $ git add . + $ git commit -m "Deploy to Heroku." + [main ...] Deploy to Heroku. + 3 files changed, 12 insertions(+), 2 deletions(-) + create mode 100644 Procfile + create mode 100644 requirements.txt + +Follow the `set-up instructions`_ to install the Heroku CLI and to log in, if +you haven't done that yet. + +.. _set-up instructions: https://devcenter.heroku.com/articles/getting-started-with-python#set-up + +Create a Heroku app. You must choose a unique name and replace +``websockets-tutorial`` by this name in the following command: + +.. code-block:: console + + $ heroku create websockets-tutorial + Creating ⬢ websockets-tutorial... done + https://websockets-tutorial.herokuapp.com/ | https://git.heroku.com/websockets-tutorial.git + +If you reuse a name that someone else already uses, you will receive this +error; if this happens, try another name: + +.. code-block:: console + + $ heroku create websockets-tutorial + Creating ⬢ websockets-tutorial... ! + ▸ Name websockets-tutorial is already taken + +Deploy by pushing the code to Heroku: + +.. code-block:: console + + $ git push heroku + + ... lots of output... + + remote: Released v1 + remote: https://websockets-tutorial.herokuapp.com/ deployed to Heroku + remote: + remote: Verifying deploy... done. + To https://git.heroku.com/websockets-tutorial.git + * [new branch] main -> main + +You can test the WebSocket server with the interactive client exactly like you +did in the first part of the tutorial. Replace ``websockets-tutorial`` by the +name of your app in the following command: + +.. code-block:: console + + $ python -m websockets wss://websockets-tutorial.herokuapp.com/ + Connected to wss://websockets-tutorial.herokuapp.com/. + > {"type": "init"} + < {"type": "init", "join": "54ICxFae_Ip7TJE2", "watch": "634w44TblL5Dbd9a"} + Connection closed: 1000 (OK). + +It works! + +Prepare the web application +--------------------------- + +Before you deploy the web application, perhaps you're wondering how it will +locate the WebSocket server? Indeed, at this point, its address is hard-coded +in ``main.js``: + +.. code-block:: javascript + + const websocket = new WebSocket("ws://localhost:8001/"); + +You can take this strategy one step further by checking the address of the +HTTP server and determining the address of the WebSocket server accordingly. + +Add this function to ``main.js``; replace ``python-websockets`` by your GitHub +username and ``websockets-tutorial`` by the name of your app on Heroku: + +.. literalinclude:: ../../example/tutorial/step3/main.js + :language: js + :start-at: function getWebSocketServer + :end-before: function initGame + +Then, update the initialization to connect to this address instead: + +.. code-block:: javascript + + const websocket = new WebSocket(getWebSocketServer()); + +Commit your changes: + +.. code-block:: console + + $ git add . + $ git commit -m "Configure WebSocket server address." + [main ...] Configure WebSocket server address. + 1 file changed, 11 insertions(+), 1 deletion(-) + +Deploy the web application +-------------------------- + +Go to GitHub and create a new repository called ``websockets-tutorial``. + +Push your code to this repository. You must replace ``python-websockets`` by +your GitHub username in the following command: + +.. code-block:: console + + $ git remote add origin git@github.com:python-websockets/websockets-tutorial.git + $ git push -u origin main + Enumerating objects: 11, done. + Counting objects: 100% (11/11), done. + Delta compression using up to 8 threads + Compressing objects: 100% (10/10), done. + Writing objects: 100% (11/11), 5.90 KiB | 2.95 MiB/s, done. + Total 11 (delta 0), reused 0 (delta 0), pack-reused 0 + To github.com:<username>/websockets-tutorial.git + * [new branch] main -> main + Branch 'main' set up to track remote branch 'main' from 'origin'. + +Go back to GitHub, open the Settings tab of the repository and select Pages in +the menu. Select the main branch as source and click Save. GitHub tells you +that your site is published. + +Follow the link and start a game! + +Summary +------- + +In this third part of the tutorial, you learned how to deploy a WebSocket +application with Heroku. + +You can start a Connect Four game, send the JOIN link to a friend, and play +over the Internet! + +Congratulations for completing the tutorial. Enjoy building real-time web +applications with websockets! + +Solution +-------- + +.. literalinclude:: ../../example/tutorial/step3/app.py + :caption: app.py + :language: python + :linenos: + +.. literalinclude:: ../../example/tutorial/step3/index.html + :caption: index.html + :language: html + :linenos: + +.. literalinclude:: ../../example/tutorial/step3/main.js + :caption: main.js + :language: js + :linenos: + +.. literalinclude:: ../../example/tutorial/step3/Procfile + :caption: Procfile + :language: text + :linenos: + +.. literalinclude:: ../../example/tutorial/step3/requirements.txt + :caption: requirements.txt + :language: text + :linenos: diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/make.bat b/tests/wpt/tests/tools/third_party/websockets/docs/make.bat new file mode 100644 index 00000000000..2119f51099b --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=.
+set BUILDDIR=_build
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+ echo.
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+ echo.installed, then set the SPHINXBUILD environment variable to point
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
+ echo.may add the Sphinx directory to PATH.
+ echo.
+ echo.If you don't have Sphinx installed, grab it from
+ echo.http://sphinx-doc.org/
+ exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
+
+:end
+popd
diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/project/changelog.rst b/tests/wpt/tests/tools/third_party/websockets/docs/project/changelog.rst new file mode 100644 index 00000000000..264e6e42d1c --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/project/changelog.rst @@ -0,0 +1,1230 @@ +Changelog +========= + +.. currentmodule:: websockets + +.. _backwards-compatibility policy: + +Backwards-compatibility policy +------------------------------ + +websockets is intended for production use. Therefore, stability is a goal. + +websockets also aims at providing the best API for WebSocket in Python. + +While we value stability, we value progress more. When an improvement requires +changing a public API, we make the change and document it in this changelog. + +When possible with reasonable effort, we preserve backwards-compatibility for +five years after the release that introduced the change. + +When a release contains backwards-incompatible API changes, the major version +is increased, else the minor version is increased. Patch versions are only for +fixing regressions shortly after a release. + +Only documented APIs are public. Undocumented, private APIs may change without +notice. + +12.0 +---- + +*October 21, 2023* + +Backwards-incompatible changes +.............................. + +.. admonition:: websockets 12.0 requires Python ≥ 3.8. + :class: tip + + websockets 11.0 is the last version supporting Python 3.7. + +Improvements +............ + +* Made convenience imports from ``websockets`` compatible with static code + analysis tools such as auto-completion in an IDE or type checking with mypy_. + + .. _mypy: https://github.com/python/mypy + +* Accepted a plain :class:`int` where an :class:`~http.HTTPStatus` is expected. + +* Added :class:`~frames.CloseCode`. + +11.0.3 +------ + +*May 7, 2023* + +Bug fixes +......... + +* Fixed the :mod:`threading` implementation of servers on Windows. + +11.0.2 +------ + +*April 18, 2023* + +Bug fixes +......... + +* Fixed a deadlock in the :mod:`threading` implementation when closing a + connection without reading all messages. + +11.0.1 +------ + +*April 6, 2023* + +Bug fixes +......... + +* Restored the C extension in the source distribution. + +11.0 +---- + +*April 2, 2023* + +Backwards-incompatible changes +.............................. + +.. admonition:: The Sans-I/O implementation was moved. + :class: caution + + Aliases provide compatibility for all previously public APIs according to + the `backwards-compatibility policy`_. + + * The ``connection`` module was renamed to ``protocol``. + + * The ``connection.Connection``, ``server.ServerConnection``, and + ``client.ClientConnection`` classes were renamed to ``protocol.Protocol``, + ``server.ServerProtocol``, and ``client.ClientProtocol``. + +.. admonition:: Sans-I/O protocol constructors now use keyword-only arguments. + :class: caution + + If you instantiate :class:`~server.ServerProtocol` or + :class:`~client.ClientProtocol` directly, make sure you are using keyword + arguments. + +.. admonition:: Closing a connection without an empty close frame is OK. + :class: note + + Receiving an empty close frame now results in + :exc:`~exceptions.ConnectionClosedOK` instead of + :exc:`~exceptions.ConnectionClosedError`. + + As a consequence, calling ``WebSocket.close()`` without arguments in a + browser isn't reported as an error anymore. + +.. admonition:: :func:`~server.serve` times out on the opening handshake after 10 seconds by default. + :class: note + + You can adjust the timeout with the ``open_timeout`` parameter. Set it to + :obj:`None` to disable the timeout entirely. + +New features +............ + +.. admonition:: websockets 11.0 introduces a implementation on top of :mod:`threading`. + :class: important + + It may be more convenient if you don't need to manage many connections and + you're more comfortable with :mod:`threading` than :mod:`asyncio`. + + It is particularly suited to client applications that establish only one + connection. It may be used for servers handling few connections. + + See :func:`~sync.client.connect` and :func:`~sync.server.serve` for details. + +* Added ``open_timeout`` to :func:`~server.serve`. + +* Made it possible to close a server without closing existing connections. + +* Added :attr:`~server.ServerProtocol.select_subprotocol` to customize + negotiation of subprotocols in the Sans-I/O layer. + +Improvements +............ + +* Added platform-independent wheels. + +* Improved error handling in :func:`~websockets.broadcast`. + +* Set ``server_hostname`` automatically on TLS connections when providing a + ``sock`` argument to :func:`~sync.client.connect`. + +10.4 +---- + +*October 25, 2022* + +New features +............ + +* Validated compatibility with Python 3.11. + +* Added the :attr:`~legacy.protocol.WebSocketCommonProtocol.latency` property to + protocols. + +* Changed :attr:`~legacy.protocol.WebSocketCommonProtocol.ping` to return the + latency of the connection. + +* Supported overriding or removing the ``User-Agent`` header in clients and the + ``Server`` header in servers. + +* Added deployment guides for more Platform as a Service providers. + +Improvements +............ + +* Improved FAQ. + +10.3 +---- + +*April 17, 2022* + +Backwards-incompatible changes +.............................. + +.. admonition:: The ``exception`` attribute of :class:`~http11.Request` and :class:`~http11.Response` is deprecated. + :class: note + + Use the ``handshake_exc`` attribute of :class:`~server.ServerProtocol` and + :class:`~client.ClientProtocol` instead. + + See :doc:`../howto/sansio` for details. + +Improvements +............ + +* Reduced noise in logs when :mod:`ssl` or :mod:`zlib` raise exceptions. + +10.2 +---- + +*February 21, 2022* + +Improvements +............ + +* Made compression negotiation more lax for compatibility with Firefox. + +* Improved FAQ and quick start guide. + +Bug fixes +......... + +* Fixed backwards-incompatibility in 10.1 for connection handlers created with + :func:`functools.partial`. + +* Avoided leaking open sockets when :func:`~client.connect` is canceled. + +10.1 +---- + +*November 14, 2021* + +New features +............ + +* Added a tutorial. + +* Made the second parameter of connection handlers optional. It will be + deprecated in the next major release. The request path is available in + the :attr:`~legacy.protocol.WebSocketCommonProtocol.path` attribute of + the first argument. + + If you implemented the connection handler of a server as:: + + async def handler(request, path): + ... + + You should replace it by:: + + async def handler(request): + path = request.path # if handler() uses the path argument + ... + +* Added ``python -m websockets --version``. + +Improvements +............ + +* Added wheels for Python 3.10, PyPy 3.7, and for more platforms. + +* Reverted optimization of default compression settings for clients, mainly to + avoid triggering bugs in poorly implemented servers like `AWS API Gateway`_. + + .. _AWS API Gateway: https://github.com/python-websockets/websockets/issues/1065 + +* Mirrored the entire :class:`~asyncio.Server` API + in :class:`~server.WebSocketServer`. + +* Improved performance for large messages on ARM processors. + +* Documented how to auto-reload on code changes in development. + +Bug fixes +......... + +* Avoided half-closing TCP connections that are already closed. + +10.0 +---- + +*September 9, 2021* + +Backwards-incompatible changes +.............................. + +.. admonition:: websockets 10.0 requires Python ≥ 3.7. + :class: tip + + websockets 9.1 is the last version supporting Python 3.6. + +.. admonition:: The ``loop`` parameter is deprecated from all APIs. + :class: caution + + This reflects a decision made in Python 3.8. See the release notes of + Python 3.10 for details. + + The ``loop`` parameter is also removed + from :class:`~server.WebSocketServer`. This should be transparent. + +.. admonition:: :func:`~client.connect` times out after 10 seconds by default. + :class: note + + You can adjust the timeout with the ``open_timeout`` parameter. Set it to + :obj:`None` to disable the timeout entirely. + +.. admonition:: The ``legacy_recv`` option is deprecated. + :class: note + + See the release notes of websockets 3.0 for details. + +.. admonition:: The signature of :exc:`~exceptions.ConnectionClosed` changed. + :class: note + + If you raise :exc:`~exceptions.ConnectionClosed` or a subclass, rather + than catch them when websockets raises them, you must change your code. + +.. admonition:: A ``msg`` parameter was added to :exc:`~exceptions.InvalidURI`. + :class: note + + If you raise :exc:`~exceptions.InvalidURI`, rather than catch it when + websockets raises it, you must change your code. + +New features +............ + +.. admonition:: websockets 10.0 introduces a `Sans-I/O API + <https://sans-io.readthedocs.io/>`_ for easier integration + in third-party libraries. + :class: important + + If you're integrating websockets in a library, rather than just using it, + look at the :doc:`Sans-I/O integration guide <../howto/sansio>`. + +* Added compatibility with Python 3.10. + +* Added :func:`~websockets.broadcast` to send a message to many clients. + +* Added support for reconnecting automatically by using + :func:`~client.connect` as an asynchronous iterator. + +* Added ``open_timeout`` to :func:`~client.connect`. + +* Documented how to integrate with `Django <https://www.djangoproject.com/>`_. + +* Documented how to deploy websockets in production, with several options. + +* Documented how to authenticate connections. + +* Documented how to broadcast messages to many connections. + +Improvements +............ + +* Improved logging. See the :doc:`logging guide <../topics/logging>`. + +* Optimized default compression settings to reduce memory usage. + +* Optimized processing of client-to-server messages when the C extension isn't + available. + +* Supported relative redirects in :func:`~client.connect`. + +* Handled TCP connection drops during the opening handshake. + +* Made it easier to customize authentication with + :meth:`~auth.BasicAuthWebSocketServerProtocol.check_credentials`. + +* Provided additional information in :exc:`~exceptions.ConnectionClosed` + exceptions. + +* Clarified several exceptions or log messages. + +* Restructured documentation. + +* Improved API documentation. + +* Extended FAQ. + +Bug fixes +......... + +* Avoided a crash when receiving a ping while the connection is closing. + +9.1 +--- + +*May 27, 2021* + +Security fix +............ + +.. admonition:: websockets 9.1 fixes a security issue introduced in 8.0. + :class: important + + Version 8.0 was vulnerable to timing attacks on HTTP Basic Auth passwords + (`CVE-2021-33880`_). + + .. _CVE-2021-33880: https://nvd.nist.gov/vuln/detail/CVE-2021-33880 + +9.0.2 +----- + +*May 15, 2021* + +Bug fixes +......... + +* Restored compatibility of ``python -m websockets`` with Python < 3.9. + +* Restored compatibility with mypy. + +9.0.1 +----- + +*May 2, 2021* + +Bug fixes +......... + +* Fixed issues with the packaging of the 9.0 release. + +9.0 +--- + +*May 1, 2021* + +Backwards-incompatible changes +.............................. + +.. admonition:: Several modules are moved or deprecated. + :class: caution + + Aliases provide compatibility for all previously public APIs according to + the `backwards-compatibility policy`_ + + * :class:`~datastructures.Headers` and + :exc:`~datastructures.MultipleValuesError` are moved from + ``websockets.http`` to :mod:`websockets.datastructures`. If you're using + them, you should adjust the import path. + + * The ``client``, ``server``, ``protocol``, and ``auth`` modules were + moved from the ``websockets`` package to a ``websockets.legacy`` + sub-package. Despite the name, they're still fully supported. + + * The ``framing``, ``handshake``, ``headers``, ``http``, and ``uri`` + modules in the ``websockets`` package are deprecated. These modules + provided low-level APIs for reuse by other projects, but they didn't + reach that goal. Keeping these APIs public makes it more difficult to + improve websockets. + + These changes pave the path for a refactoring that should be a transparent + upgrade for most uses and facilitate integration by other projects. + +.. admonition:: Convenience imports from ``websockets`` are performed lazily. + :class: note + + While Python supports this, tools relying on static code analysis don't. + This breaks auto-completion in an IDE or type checking with mypy_. + + .. _mypy: https://github.com/python/mypy + + If you depend on such tools, use the real import paths, which can be found + in the API documentation, for example:: + + from websockets.client import connect + from websockets.server import serve + +New features +............ + +* Added compatibility with Python 3.9. + +Improvements +............ + +* Added support for IRIs in addition to URIs. + +* Added close codes 1012, 1013, and 1014. + +* Raised an error when passing a :class:`dict` to + :meth:`~legacy.protocol.WebSocketCommonProtocol.send`. + +* Improved error reporting. + +Bug fixes +......... + +* Fixed sending fragmented, compressed messages. + +* Fixed ``Host`` header sent when connecting to an IPv6 address. + +* Fixed creating a client or a server with an existing Unix socket. + +* Aligned maximum cookie size with popular web browsers. + +* Ensured cancellation always propagates, even on Python versions where + :exc:`~asyncio.CancelledError` inherits :exc:`Exception`. + +8.1 +--- + +*November 1, 2019* + +New features +............ + +* Added compatibility with Python 3.8. + +8.0.2 +----- + +*July 31, 2019* + +Bug fixes +......... + +* Restored the ability to pass a socket with the ``sock`` parameter of + :func:`~server.serve`. + +* Removed an incorrect assertion when a connection drops. + +8.0.1 +----- + +*July 21, 2019* + +Bug fixes +......... + +* Restored the ability to import ``WebSocketProtocolError`` from + ``websockets``. + +8.0 +--- + +*July 7, 2019* + +Backwards-incompatible changes +.............................. + +.. admonition:: websockets 8.0 requires Python ≥ 3.6. + :class: tip + + websockets 7.0 is the last version supporting Python 3.4 and 3.5. + +.. admonition:: ``process_request`` is now expected to be a coroutine. + :class: note + + If you're passing a ``process_request`` argument to + :func:`~server.serve` or :class:`~server.WebSocketServerProtocol`, or if + you're overriding + :meth:`~server.WebSocketServerProtocol.process_request` in a subclass, + define it with ``async def`` instead of ``def``. Previously, both were supported. + + For backwards compatibility, functions are still accepted, but mixing + functions and coroutines won't work in some inheritance scenarios. + +.. admonition:: ``max_queue`` must be :obj:`None` to disable the limit. + :class: note + + If you were setting ``max_queue=0`` to make the queue of incoming messages + unbounded, change it to ``max_queue=None``. + +.. admonition:: The ``host``, ``port``, and ``secure`` attributes + of :class:`~legacy.protocol.WebSocketCommonProtocol` are deprecated. + :class: note + + Use :attr:`~legacy.protocol.WebSocketCommonProtocol.local_address` in + servers and + :attr:`~legacy.protocol.WebSocketCommonProtocol.remote_address` in clients + instead of ``host`` and ``port``. + +.. admonition:: ``WebSocketProtocolError`` is renamed + to :exc:`~exceptions.ProtocolError`. + :class: note + + An alias provides backwards compatibility. + +.. admonition:: ``read_response()`` now returns the reason phrase. + :class: note + + If you're using this low-level API, you must change your code. + +New features +............ + +* Added :func:`~auth.basic_auth_protocol_factory` to enforce HTTP + Basic Auth on the server side. + +* :func:`~client.connect` handles redirects from the server during the + handshake. + +* :func:`~client.connect` supports overriding ``host`` and ``port``. + +* Added :func:`~client.unix_connect` for connecting to Unix sockets. + +* Added support for asynchronous generators + in :meth:`~legacy.protocol.WebSocketCommonProtocol.send` + to generate fragmented messages incrementally. + +* Enabled readline in the interactive client. + +* Added type hints (:pep:`484`). + +* Added a FAQ to the documentation. + +* Added documentation for extensions. + +* Documented how to optimize memory usage. + +Improvements +............ + +* :meth:`~legacy.protocol.WebSocketCommonProtocol.send`, + :meth:`~legacy.protocol.WebSocketCommonProtocol.ping`, and + :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` support bytes-like + types :class:`bytearray` and :class:`memoryview` in addition to + :class:`bytes`. + +* Added :exc:`~exceptions.ConnectionClosedOK` and + :exc:`~exceptions.ConnectionClosedError` subclasses of + :exc:`~exceptions.ConnectionClosed` to tell apart normal connection + termination from errors. + +* Changed :meth:`WebSocketServer.close() + <server.WebSocketServer.close>` to perform a proper closing handshake + instead of failing the connection. + +* Improved error messages when HTTP parsing fails. + +* Improved API documentation. + +Bug fixes +......... + +* Prevented spurious log messages about :exc:`~exceptions.ConnectionClosed` + exceptions in keepalive ping task. If you were using ``ping_timeout=None`` + as a workaround, you can remove it. + +* Avoided a crash when a ``extra_headers`` callable returns :obj:`None`. + +7.0 +--- + +*November 1, 2018* + +Backwards-incompatible changes +.............................. + +.. admonition:: Keepalive is enabled by default. + :class: important + + websockets now sends Ping frames at regular intervals and closes the + connection if it doesn't receive a matching Pong frame. + See :class:`~legacy.protocol.WebSocketCommonProtocol` for details. + +.. admonition:: Termination of connections by :meth:`WebSocketServer.close() + <server.WebSocketServer.close>` changes. + :class: caution + + Previously, connections handlers were canceled. Now, connections are + closed with close code 1001 (going away). + + From the perspective of the connection handler, this is the same as if the + remote endpoint was disconnecting. This removes the need to prepare for + :exc:`~asyncio.CancelledError` in connection handlers. + + You can restore the previous behavior by adding the following line at the + beginning of connection handlers:: + + def handler(websocket, path): + closed = asyncio.ensure_future(websocket.wait_closed()) + closed.add_done_callback(lambda task: task.cancel()) + +.. admonition:: Calling :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` + concurrently raises a :exc:`RuntimeError`. + :class: note + + Concurrent calls lead to non-deterministic behavior because there are no + guarantees about which coroutine will receive which message. + +.. admonition:: The ``timeout`` argument of :func:`~server.serve` + and :func:`~client.connect` is renamed to ``close_timeout`` . + :class: note + + This prevents confusion with ``ping_timeout``. + + For backwards compatibility, ``timeout`` is still supported. + +.. admonition:: The ``origins`` argument of :func:`~server.serve` changes. + :class: note + + Include :obj:`None` in the list rather than ``''`` to allow requests that + don't contain an Origin header. + +.. admonition:: Pending pings aren't canceled when the connection is closed. + :class: note + + A ping — as in ``ping = await websocket.ping()`` — for which no pong was + received yet used to be canceled when the connection is closed, so that + ``await ping`` raised :exc:`~asyncio.CancelledError`. + + Now ``await ping`` raises :exc:`~exceptions.ConnectionClosed` like other + public APIs. + +New features +............ + +* Added ``process_request`` and ``select_subprotocol`` arguments to + :func:`~server.serve` and + :class:`~server.WebSocketServerProtocol` to facilitate customization of + :meth:`~server.WebSocketServerProtocol.process_request` and + :meth:`~server.WebSocketServerProtocol.select_subprotocol`. + +* Added support for sending fragmented messages. + +* Added the :meth:`~legacy.protocol.WebSocketCommonProtocol.wait_closed` + method to protocols. + +* Added an interactive client: ``python -m websockets <uri>``. + +Improvements +............ + +* Improved handling of multiple HTTP headers with the same name. + +* Improved error messages when a required HTTP header is missing. + +Bug fixes +......... + +* Fixed a data loss bug in + :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`: + canceling it at the wrong time could result in messages being dropped. + +6.0 +--- + +*July 16, 2018* + +Backwards-incompatible changes +.............................. + +.. admonition:: The :class:`~datastructures.Headers` class is introduced and + several APIs are updated to use it. + :class: caution + + * The ``request_headers`` argument + of :meth:`~server.WebSocketServerProtocol.process_request` is now + a :class:`~datastructures.Headers` instead of + an ``http.client.HTTPMessage``. + + * The ``request_headers`` and ``response_headers`` attributes of + :class:`~legacy.protocol.WebSocketCommonProtocol` are now + :class:`~datastructures.Headers` instead of ``http.client.HTTPMessage``. + + * The ``raw_request_headers`` and ``raw_response_headers`` attributes of + :class:`~legacy.protocol.WebSocketCommonProtocol` are removed. Use + :meth:`~datastructures.Headers.raw_items` instead. + + * Functions defined in the ``handshake`` module now receive + :class:`~datastructures.Headers` in argument instead of ``get_header`` + or ``set_header`` functions. This affects libraries that rely on + low-level APIs. + + * Functions defined in the ``http`` module now return HTTP headers as + :class:`~datastructures.Headers` instead of lists of ``(name, value)`` + pairs. + + Since :class:`~datastructures.Headers` and ``http.client.HTTPMessage`` + provide similar APIs, much of the code dealing with HTTP headers won't + require changes. + +New features +............ + +* Added compatibility with Python 3.7. + +5.0.1 +----- + +*May 24, 2018* + +Bug fixes +......... + +* Fixed a regression in 5.0 that broke some invocations of + :func:`~server.serve` and :func:`~client.connect`. + +5.0 +--- + +*May 22, 2018* + +Security fix +............ + +.. admonition:: websockets 5.0 fixes a security issue introduced in 4.0. + :class: important + + Version 4.0 was vulnerable to denial of service by memory exhaustion + because it didn't enforce ``max_size`` when decompressing compressed + messages (`CVE-2018-1000518`_). + + .. _CVE-2018-1000518: https://nvd.nist.gov/vuln/detail/CVE-2018-1000518 + +Backwards-incompatible changes +.............................. + +.. admonition:: A ``user_info`` field is added to the return value of + ``parse_uri`` and ``WebSocketURI``. + :class: note + + If you're unpacking ``WebSocketURI`` into four variables, adjust your code + to account for that fifth field. + +New features +............ + +* :func:`~client.connect` performs HTTP Basic Auth when the URI contains + credentials. + +* :func:`~server.unix_serve` can be used as an asynchronous context + manager on Python ≥ 3.5.1. + +* Added the :attr:`~legacy.protocol.WebSocketCommonProtocol.closed` property + to protocols. + +* Added new examples in the documentation. + +Improvements +............ + +* Iterating on incoming messages no longer raises an exception when the + connection terminates with close code 1001 (going away). + +* A plain HTTP request now receives a 426 Upgrade Required response and + doesn't log a stack trace. + +* If a :meth:`~legacy.protocol.WebSocketCommonProtocol.ping` doesn't receive a + pong, it's canceled when the connection is closed. + +* Reported the cause of :exc:`~exceptions.ConnectionClosed` exceptions. + +* Stopped logging stack traces when the TCP connection dies prematurely. + +* Prevented writing to a closing TCP connection during unclean shutdowns. + +* Made connection termination more robust to network congestion. + +* Prevented processing of incoming frames after failing the connection. + +* Updated documentation with new features from Python 3.6. + +* Improved several sections of the documentation. + +Bug fixes +......... + +* Prevented :exc:`TypeError` due to missing close code on connection close. + +* Fixed a race condition in the closing handshake that raised + :exc:`~exceptions.InvalidState`. + +4.0.1 +----- + +*November 2, 2017* + +Bug fixes +......... + +* Fixed issues with the packaging of the 4.0 release. + +4.0 +--- + +*November 2, 2017* + +Backwards-incompatible changes +.............................. + +.. admonition:: websockets 4.0 requires Python ≥ 3.4. + :class: tip + + websockets 3.4 is the last version supporting Python 3.3. + +.. admonition:: Compression is enabled by default. + :class: important + + In August 2017, Firefox and Chrome support the permessage-deflate + extension, but not Safari and IE. + + Compression should improve performance but it increases RAM and CPU use. + + If you want to disable compression, add ``compression=None`` when calling + :func:`~server.serve` or :func:`~client.connect`. + +.. admonition:: The ``state_name`` attribute of protocols is deprecated. + :class: note + + Use ``protocol.state.name`` instead of ``protocol.state_name``. + +New features +............ + +* :class:`~legacy.protocol.WebSocketCommonProtocol` instances can be used as + asynchronous iterators on Python ≥ 3.6. They yield incoming messages. + +* Added :func:`~server.unix_serve` for listening on Unix sockets. + +* Added the :attr:`~server.WebSocketServer.sockets` attribute to the + return value of :func:`~server.serve`. + +* Allowed ``extra_headers`` to override ``Server`` and ``User-Agent`` headers. + +Improvements +............ + +* Reorganized and extended documentation. + +* Rewrote connection termination to increase robustness in edge cases. + +* Reduced verbosity of "Failing the WebSocket connection" logs. + +Bug fixes +......... + +* Aborted connections if they don't close within the configured ``timeout``. + +* Stopped leaking pending tasks when :meth:`~asyncio.Task.cancel` is called on + a connection while it's being closed. + +3.4 +--- + +*August 20, 2017* + +Backwards-incompatible changes +.............................. + +.. admonition:: ``InvalidStatus`` is replaced + by :class:`~exceptions.InvalidStatusCode`. + :class: note + + This exception is raised when :func:`~client.connect` receives an invalid + response status code from the server. + +New features +............ + +* :func:`~server.serve` can be used as an asynchronous context manager + on Python ≥ 3.5.1. + +* Added support for customizing handling of incoming connections with + :meth:`~server.WebSocketServerProtocol.process_request`. + +* Made read and write buffer sizes configurable. + +Improvements +............ + +* Renamed :func:`~server.serve` and :func:`~client.connect`'s + ``klass`` argument to ``create_protocol`` to reflect that it can also be a + callable. For backwards compatibility, ``klass`` is still supported. + +* Rewrote HTTP handling for simplicity and performance. + +* Added an optional C extension to speed up low-level operations. + +Bug fixes +......... + +* Providing a ``sock`` argument to :func:`~client.connect` no longer + crashes. + +3.3 +--- + +*March 29, 2017* + +New features +............ + +* Ensured compatibility with Python 3.6. + +Improvements +............ + +* Reduced noise in logs caused by connection resets. + +Bug fixes +......... + +* Avoided crashing on concurrent writes on slow connections. + +3.2 +--- + +*August 17, 2016* + +New features +............ + +* Added ``timeout``, ``max_size``, and ``max_queue`` arguments to + :func:`~client.connect` and :func:`~server.serve`. + +Improvements +............ + +* Made server shutdown more robust. + +3.1 +--- + +*April 21, 2016* + +New features +............ + +* Added flow control for incoming data. + +Bug fixes +......... + +* Avoided a warning when closing a connection before the opening handshake. + +3.0 +--- + +*December 25, 2015* + +Backwards-incompatible changes +.............................. + +.. admonition:: :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` now + raises an exception when the connection is closed. + :class: caution + + :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` used to return + :obj:`None` when the connection was closed. This required checking the + return value of every call:: + + message = await websocket.recv() + if message is None: + return + + Now it raises a :exc:`~exceptions.ConnectionClosed` exception instead. + This is more Pythonic. The previous code can be simplified to:: + + message = await websocket.recv() + + When implementing a server, there's no strong reason to handle such + exceptions. Let them bubble up, terminate the handler coroutine, and the + server will simply ignore them. + + In order to avoid stranding projects built upon an earlier version, the + previous behavior can be restored by passing ``legacy_recv=True`` to + :func:`~server.serve`, :func:`~client.connect`, + :class:`~server.WebSocketServerProtocol`, or + :class:`~client.WebSocketClientProtocol`. + +New features +............ + +* :func:`~client.connect` can be used as an asynchronous context + manager on Python ≥ 3.5.1. + +* :meth:`~legacy.protocol.WebSocketCommonProtocol.ping` and + :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` support data passed as + :class:`str` in addition to :class:`bytes`. + +* Made ``state_name`` attribute on protocols a public API. + +Improvements +............ + +* Updated documentation with ``await`` and ``async`` syntax from Python 3.5. + +* Worked around an :mod:`asyncio` bug affecting connection termination under + load. + +* Improved documentation. + +2.7 +--- + +*November 18, 2015* + +New features +............ + +* Added compatibility with Python 3.5. + +Improvements +............ + +* Refreshed documentation. + +2.6 +--- + +*August 18, 2015* + +New features +............ + +* Added ``local_address`` and ``remote_address`` attributes on protocols. + +* Closed open connections with code 1001 when a server shuts down. + +Bug fixes +......... + +* Avoided TCP fragmentation of small frames. + +2.5 +--- + +*July 28, 2015* + +New features +............ + +* Provided access to handshake request and response HTTP headers. + +* Allowed customizing handshake request and response HTTP headers. + +* Added support for running on a non-default event loop. + +Improvements +............ + +* Improved documentation. + +* Sent a 403 status code instead of 400 when request Origin isn't allowed. + +* Clarified that the closing handshake can be initiated by the client. + +* Set the close code and reason more consistently. + +* Strengthened connection termination. + +Bug fixes +......... + +* Canceling :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` no longer + drops the next message. + +2.4 +--- + +*January 31, 2015* + +New features +............ + +* Added support for subprotocols. + +* Added ``loop`` argument to :func:`~client.connect` and + :func:`~server.serve`. + +2.3 +--- + +*November 3, 2014* + +Improvements +............ + +* Improved compliance of close codes. + +2.2 +--- + +*July 28, 2014* + +New features +............ + +* Added support for limiting message size. + +2.1 +--- + +*April 26, 2014* + +New features +............ + +* Added ``host``, ``port`` and ``secure`` attributes on protocols. + +* Added support for providing and checking Origin_. + +.. _Origin: https://www.rfc-editor.org/rfc/rfc6455.html#section-10.2 + +2.0 +--- + +*February 16, 2014* + +Backwards-incompatible changes +.............................. + +.. admonition:: :meth:`~legacy.protocol.WebSocketCommonProtocol.send`, + :meth:`~legacy.protocol.WebSocketCommonProtocol.ping`, and + :meth:`~legacy.protocol.WebSocketCommonProtocol.pong` are now coroutines. + :class: caution + + They used to be functions. + + Instead of:: + + websocket.send(message) + + you must write:: + + await websocket.send(message) + +New features +............ + +* Added flow control for outgoing data. + +1.0 +--- + +*November 14, 2013* + +New features +............ + +* Initial public release. diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/project/contributing.rst b/tests/wpt/tests/tools/third_party/websockets/docs/project/contributing.rst new file mode 100644 index 00000000000..020ed7ad85c --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/project/contributing.rst @@ -0,0 +1,66 @@ +Contributing +============ + +Thanks for taking the time to contribute to websockets! + +Code of Conduct +--------------- + +This project and everyone participating in it is governed by the `Code of +Conduct`_. By participating, you are expected to uphold this code. Please +report inappropriate behavior to aymeric DOT augustin AT fractalideas DOT com. + +.. _Code of Conduct: https://github.com/python-websockets/websockets/blob/main/CODE_OF_CONDUCT.md + +*(If I'm the person with the inappropriate behavior, please accept my +apologies. I know I can mess up. I can't expect you to tell me, but if you +choose to do so, I'll do my best to handle criticism constructively. +-- Aymeric)* + +Contributions +------------- + +Bug reports, patches and suggestions are welcome! + +Please open an issue_ or send a `pull request`_. + +Feedback about the documentation is especially valuable, as the primary author +feels more confident about writing code than writing docs :-) + +If you're wondering why things are done in a certain way, the :doc:`design +document <../topics/design>` provides lots of details about the internals of +websockets. + +.. _issue: https://github.com/python-websockets/websockets/issues/new +.. _pull request: https://github.com/python-websockets/websockets/compare/ + +Questions +--------- + +GitHub issues aren't a good medium for handling questions. There are better +places to ask questions, for example Stack Overflow. + +If you want to ask a question anyway, please make sure that: + +- it's a question about websockets and not about :mod:`asyncio`; +- it isn't answered in the documentation; +- it wasn't asked already. + +A good question can be written as a suggestion to improve the documentation. + +Cryptocurrency users +-------------------- + +websockets appears to be quite popular for interfacing with Bitcoin or other +cryptocurrency trackers. I'm strongly opposed to Bitcoin's carbon footprint. + +I'm aware of efforts to build proof-of-stake models. I'll care once the total +energy consumption of all cryptocurrencies drops to a non-bullshit level. + +You already negated all of humanity's efforts to develop renewable energy. +Please stop heating the planet where my children will have to live. + +Since websockets is released under an open-source license, you can use it for +any purpose you like. However, I won't spend any of my time to help you. + +I will summarily close issues related to Bitcoin or cryptocurrency in any way. diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/project/index.rst b/tests/wpt/tests/tools/third_party/websockets/docs/project/index.rst new file mode 100644 index 00000000000..459146345b0 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/project/index.rst @@ -0,0 +1,12 @@ +About websockets +================ + +This is about websockets-the-project rather than websockets-the-software. + +.. toctree:: + :titlesonly: + + changelog + contributing + license + For enterprise <tidelift> diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/project/license.rst b/tests/wpt/tests/tools/third_party/websockets/docs/project/license.rst new file mode 100644 index 00000000000..0a3b8703d56 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/project/license.rst @@ -0,0 +1,4 @@ +License +======= + +.. include:: ../../LICENSE diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/project/tidelift.rst b/tests/wpt/tests/tools/third_party/websockets/docs/project/tidelift.rst new file mode 100644 index 00000000000..42100fade9b --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/project/tidelift.rst @@ -0,0 +1,112 @@ +websockets for enterprise +========================= + +Available as part of the Tidelift Subscription +---------------------------------------------- + +.. image:: ../_static/tidelift.png + :height: 150px + :width: 150px + :align: left + +Tidelift is working with the maintainers of websockets and thousands of other +open source projects to deliver commercial support and maintenance for the +open source dependencies you use to build your applications. Save time, reduce +risk, and improve code health, while paying the maintainers of the exact +dependencies you use. + +.. raw:: html + + <style type="text/css"> + .tidelift-links { + display: flex; + justify-content: center; + } + @media only screen and (max-width: 600px) { + .tidelift-links { + flex-direction: column; + } + } + .tidelift-links a { + border: thin solid #f6914d; + border-radius: 0.25em; + font-family: Verdana, sans-serif; + font-size: 15px; + margin: 0.5em 2em; + padding: 0.5em 2em; + text-align: center; + text-decoration: none; + text-transform: uppercase; + } + .tidelift-links a.tidelift-links__learn-more { + background-color: white; + color: #f6914d; + } + .tidelift-links a.tidelift-links__request-a-demo { + background-color: #f6914d; + color: white; + } + </style> + + <div class="tidelift-links"> + <a class="tidelift-links__learn-more" href="https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=enterprise">Learn more</a> + <a class="tidelift-links__request-a-demo" href="https://tidelift.com/subscription/request-a-demo?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=enterprise">Request a demo</a> + </div> + +Enterprise-ready open source software—managed for you +----------------------------------------------------- + +The Tidelift Subscription is a managed open source subscription for +application dependencies covering millions of open source projects across +JavaScript, Python, Java, PHP, Ruby, .NET, and more. + +Your subscription includes: + +* **Security updates** + + * Tidelift’s security response team coordinates patches for new breaking + security vulnerabilities and alerts immediately through a private channel, + so your software supply chain is always secure. + +* **Licensing verification and indemnification** + + * Tidelift verifies license information to enable easy policy enforcement + and adds intellectual property indemnification to cover creators and users + in case something goes wrong. You always have a 100% up-to-date bill of + materials for your dependencies to share with your legal team, customers, + or partners. + +* **Maintenance and code improvement** + + * Tidelift ensures the software you rely on keeps working as long as you + need it to work. Your managed dependencies are actively maintained and we + recruit additional maintainers where required. + +* **Package selection and version guidance** + + * We help you choose the best open source packages from the start—and then + guide you through updates to stay on the best releases as new issues + arise. + +* **Roadmap input** + + * Take a seat at the table with the creators behind the software you use. + Tidelift’s participating maintainers earn more income as their software is + used by more subscribers, so they’re interested in knowing what you need. + +* **Tooling and cloud integration** + + * Tidelift works with GitHub, GitLab, BitBucket, and more. We support every + cloud platform (and other deployment targets, too). + +The end result? All of the capabilities you expect from commercial-grade +software, for the full breadth of open source you use. That means less time +grappling with esoteric open source trivia, and more time building your own +applications—and your business. + +.. raw:: html + + <div class="tidelift-links"> + <a class="tidelift-links__learn-more" href="https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=enterprise">Learn more</a> + <a class="tidelift-links__request-a-demo" href="https://tidelift.com/subscription/request-a-demo?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=enterprise">Request a demo</a> + </div> diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/reference/asyncio/client.rst b/tests/wpt/tests/tools/third_party/websockets/docs/reference/asyncio/client.rst new file mode 100644 index 00000000000..5086015b7b9 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/reference/asyncio/client.rst @@ -0,0 +1,64 @@ +Client (:mod:`asyncio`) +======================= + +.. automodule:: websockets.client + +Opening a connection +-------------------- + +.. autofunction:: connect(uri, *, create_protocol=None, logger=None, compression="deflate", origin=None, extensions=None, subprotocols=None, extra_headers=None, user_agent_header="Python/x.y.z websockets/X.Y", open_timeout=10, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, **kwds) + :async: + +.. autofunction:: unix_connect(path, uri="ws://localhost/", *, create_protocol=None, logger=None, compression="deflate", origin=None, extensions=None, subprotocols=None, extra_headers=None, user_agent_header="Python/x.y.z websockets/X.Y", open_timeout=10, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, **kwds) + :async: + +Using a connection +------------------ + +.. autoclass:: WebSocketClientProtocol(*, logger=None, origin=None, extensions=None, subprotocols=None, extra_headers=None, user_agent_header="Python/x.y.z websockets/X.Y", ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16) + + .. automethod:: recv + + .. automethod:: send + + .. automethod:: close + + .. automethod:: wait_closed + + .. automethod:: ping + + .. automethod:: pong + + WebSocket connection objects also provide these attributes: + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: local_address + + .. autoproperty:: remote_address + + .. autoproperty:: open + + .. autoproperty:: closed + + .. autoattribute:: latency + + The following attributes are available after the opening handshake, + once the WebSocket connection is open: + + .. autoattribute:: path + + .. autoattribute:: request_headers + + .. autoattribute:: response_headers + + .. autoattribute:: subprotocol + + The following attributes are available after the closing handshake, + once the WebSocket connection is closed: + + .. autoproperty:: close_code + + .. autoproperty:: close_reason diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/reference/asyncio/common.rst b/tests/wpt/tests/tools/third_party/websockets/docs/reference/asyncio/common.rst new file mode 100644 index 00000000000..dc7a54ee1ab --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/reference/asyncio/common.rst @@ -0,0 +1,54 @@ +:orphan: + +Both sides (:mod:`asyncio`) +=========================== + +.. automodule:: websockets.legacy.protocol + +.. autoclass:: WebSocketCommonProtocol(*, logger=None, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16) + + .. automethod:: recv + + .. automethod:: send + + .. automethod:: close + + .. automethod:: wait_closed + + .. automethod:: ping + + .. automethod:: pong + + WebSocket connection objects also provide these attributes: + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: local_address + + .. autoproperty:: remote_address + + .. autoproperty:: open + + .. autoproperty:: closed + + .. autoattribute:: latency + + The following attributes are available after the opening handshake, + once the WebSocket connection is open: + + .. autoattribute:: path + + .. autoattribute:: request_headers + + .. autoattribute:: response_headers + + .. autoattribute:: subprotocol + + The following attributes are available after the closing handshake, + once the WebSocket connection is closed: + + .. autoproperty:: close_code + + .. autoproperty:: close_reason diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/reference/asyncio/server.rst b/tests/wpt/tests/tools/third_party/websockets/docs/reference/asyncio/server.rst new file mode 100644 index 00000000000..10631791626 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/reference/asyncio/server.rst @@ -0,0 +1,113 @@ +Server (:mod:`asyncio`) +======================= + +.. automodule:: websockets.server + +Starting a server +----------------- + +.. autofunction:: serve(ws_handler, host=None, port=None, *, create_protocol=None, logger=None, compression="deflate", origins=None, extensions=None, subprotocols=None, extra_headers=None, server_header="Python/x.y.z websockets/X.Y", process_request=None, select_subprotocol=None, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, **kwds) + :async: + +.. autofunction:: unix_serve(ws_handler, path=None, *, create_protocol=None, logger=None, compression="deflate", origins=None, extensions=None, subprotocols=None, extra_headers=None, server_header="Python/x.y.z websockets/X.Y", process_request=None, select_subprotocol=None, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16, **kwds) + :async: + +Stopping a server +----------------- + +.. autoclass:: WebSocketServer + + .. automethod:: close + + .. automethod:: wait_closed + + .. automethod:: get_loop + + .. automethod:: is_serving + + .. automethod:: start_serving + + .. automethod:: serve_forever + + .. autoattribute:: sockets + +Using a connection +------------------ + +.. autoclass:: WebSocketServerProtocol(ws_handler, ws_server, *, logger=None, origins=None, extensions=None, subprotocols=None, extra_headers=None, server_header="Python/x.y.z websockets/X.Y", process_request=None, select_subprotocol=None, ping_interval=20, ping_timeout=20, close_timeout=10, max_size=2 ** 20, max_queue=2 ** 5, read_limit=2 ** 16, write_limit=2 ** 16) + + .. automethod:: recv + + .. automethod:: send + + .. automethod:: close + + .. automethod:: wait_closed + + .. automethod:: ping + + .. automethod:: pong + + You can customize the opening handshake in a subclass by overriding these methods: + + .. automethod:: process_request + + .. automethod:: select_subprotocol + + WebSocket connection objects also provide these attributes: + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: local_address + + .. autoproperty:: remote_address + + .. autoproperty:: open + + .. autoproperty:: closed + + .. autoattribute:: latency + + The following attributes are available after the opening handshake, + once the WebSocket connection is open: + + .. autoattribute:: path + + .. autoattribute:: request_headers + + .. autoattribute:: response_headers + + .. autoattribute:: subprotocol + + The following attributes are available after the closing handshake, + once the WebSocket connection is closed: + + .. autoproperty:: close_code + + .. autoproperty:: close_reason + + +Basic authentication +-------------------- + +.. automodule:: websockets.auth + +websockets supports HTTP Basic Authentication according to +:rfc:`7235` and :rfc:`7617`. + +.. autofunction:: basic_auth_protocol_factory + +.. autoclass:: BasicAuthWebSocketServerProtocol + + .. autoattribute:: realm + + .. autoattribute:: username + + .. automethod:: check_credentials + +Broadcast +--------- + +.. autofunction:: websockets.broadcast diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/reference/datastructures.rst b/tests/wpt/tests/tools/third_party/websockets/docs/reference/datastructures.rst new file mode 100644 index 00000000000..ec02d421015 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/reference/datastructures.rst @@ -0,0 +1,66 @@ +Data structures +=============== + +WebSocket events +---------------- + +.. automodule:: websockets.frames + + .. autoclass:: Frame + + .. autoclass:: Opcode + + .. autoattribute:: CONT + .. autoattribute:: TEXT + .. autoattribute:: BINARY + .. autoattribute:: CLOSE + .. autoattribute:: PING + .. autoattribute:: PONG + + .. autoclass:: Close + + .. autoclass:: CloseCode + + .. autoattribute:: NORMAL_CLOSURE + .. autoattribute:: GOING_AWAY + .. autoattribute:: PROTOCOL_ERROR + .. autoattribute:: UNSUPPORTED_DATA + .. autoattribute:: NO_STATUS_RCVD + .. autoattribute:: ABNORMAL_CLOSURE + .. autoattribute:: INVALID_DATA + .. autoattribute:: POLICY_VIOLATION + .. autoattribute:: MESSAGE_TOO_BIG + .. autoattribute:: MANDATORY_EXTENSION + .. autoattribute:: INTERNAL_ERROR + .. autoattribute:: SERVICE_RESTART + .. autoattribute:: TRY_AGAIN_LATER + .. autoattribute:: BAD_GATEWAY + .. autoattribute:: TLS_HANDSHAKE + +HTTP events +----------- + +.. automodule:: websockets.http11 + + .. autoclass:: Request + + .. autoclass:: Response + +.. automodule:: websockets.datastructures + + .. autoclass:: Headers + + .. automethod:: get_all + + .. automethod:: raw_items + + .. autoexception:: MultipleValuesError + +URIs +---- + +.. automodule:: websockets.uri + + .. autofunction:: parse_uri + + .. autoclass:: WebSocketURI diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/reference/exceptions.rst b/tests/wpt/tests/tools/third_party/websockets/docs/reference/exceptions.rst new file mode 100644 index 00000000000..907a650d204 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/reference/exceptions.rst @@ -0,0 +1,6 @@ +Exceptions +========== + +.. automodule:: websockets.exceptions + :members: + diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/reference/extensions.rst b/tests/wpt/tests/tools/third_party/websockets/docs/reference/extensions.rst new file mode 100644 index 00000000000..a70f1b1e58a --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/reference/extensions.rst @@ -0,0 +1,60 @@ +Extensions +========== + +.. currentmodule:: websockets.extensions + +The WebSocket protocol supports extensions_. + +At the time of writing, there's only one `registered extension`_ with a public +specification, WebSocket Per-Message Deflate. + +.. _extensions: https://www.rfc-editor.org/rfc/rfc6455.html#section-9 +.. _registered extension: https://www.iana.org/assignments/websocket/websocket.xhtml#extension-name + +Per-Message Deflate +------------------- + +.. automodule:: websockets.extensions.permessage_deflate + + :mod:`websockets.extensions.permessage_deflate` implements WebSocket + Per-Message Deflate. + + This extension is specified in :rfc:`7692`. + + Refer to the :doc:`topic guide on compression <../topics/compression>` to + learn more about tuning compression settings. + + .. autoclass:: ClientPerMessageDeflateFactory + + .. autoclass:: ServerPerMessageDeflateFactory + +Base classes +------------ + +.. automodule:: websockets.extensions + + :mod:`websockets.extensions` defines base classes for implementing + extensions. + + Refer to the :doc:`how-to guide on extensions <../howto/extensions>` to + learn more about writing an extension. + + .. autoclass:: Extension + + .. autoattribute:: name + + .. automethod:: decode + + .. automethod:: encode + + .. autoclass:: ClientExtensionFactory + + .. autoattribute:: name + + .. automethod:: get_request_params + + .. automethod:: process_response_params + + .. autoclass:: ServerExtensionFactory + + .. automethod:: process_request_params diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/reference/features.rst b/tests/wpt/tests/tools/third_party/websockets/docs/reference/features.rst new file mode 100644 index 00000000000..98b3c0ddafc --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/reference/features.rst @@ -0,0 +1,187 @@ +Features +======== + +.. currentmodule:: websockets + +Feature support matrices summarize which implementations support which features. + +.. raw:: html + + <style> + .support-matrix-table { width: 100%; } + .support-matrix-table th:first-child { text-align: left; } + .support-matrix-table th:not(:first-child) { text-align: center; width: 15%; } + .support-matrix-table td:not(:first-child) { text-align: center; } + </style> + +.. |aio| replace:: :mod:`asyncio` +.. |sync| replace:: :mod:`threading` +.. |sans| replace:: `Sans-I/O`_ +.. _Sans-I/O: https://sans-io.readthedocs.io/ + +Both sides +---------- + +.. table:: + :class: support-matrix-table + + +------------------------------------+--------+--------+--------+ + | | |aio| | |sync| | |sans| | + +====================================+========+========+========+ + | Perform the opening handshake | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Send a message | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Receive a message | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Iterate over received messages | ✅ | ✅ | ❌ | + +------------------------------------+--------+--------+--------+ + | Send a fragmented message | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Receive a fragmented message after | ✅ | ✅ | ❌ | + | reassembly | | | | + +------------------------------------+--------+--------+--------+ + | Receive a fragmented message frame | ❌ | ✅ | ✅ | + | by frame (`#479`_) | | | | + +------------------------------------+--------+--------+--------+ + | Send a ping | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Respond to pings automatically | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Send a pong | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Perform the closing handshake | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Report close codes and reasons | ❌ | ✅ | ✅ | + | from both sides | | | | + +------------------------------------+--------+--------+--------+ + | Compress messages (:rfc:`7692`) | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Tune memory usage for compression | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Negotiate extensions | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Implement custom extensions | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Negotiate a subprotocol | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Enforce security limits | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Log events | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Enforce opening timeout | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Enforce closing timeout | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Keepalive | ✅ | ❌ | — | + +------------------------------------+--------+--------+--------+ + | Heartbeat | ✅ | ❌ | — | + +------------------------------------+--------+--------+--------+ + +.. _#479: https://github.com/python-websockets/websockets/issues/479 + +Server +------ + +.. table:: + :class: support-matrix-table + + +------------------------------------+--------+--------+--------+ + | | |aio| | |sync| | |sans| | + +====================================+========+========+========+ + | Listen on a TCP socket | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Listen on a Unix socket | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Listen using a preexisting socket | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Encrypt connection with TLS | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Close server on context exit | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Close connection on handler exit | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Shut down server gracefully | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Check ``Origin`` header | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Customize subprotocol selection | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Configure ``Server`` header | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Alter opening handshake request | ❌ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Alter opening handshake response | ❌ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Perform HTTP Basic Authentication | ✅ | ❌ | ❌ | + +------------------------------------+--------+--------+--------+ + | Perform HTTP Digest Authentication | ❌ | ❌ | ❌ | + +------------------------------------+--------+--------+--------+ + | Force HTTP response | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + +Client +------ + +.. table:: + :class: support-matrix-table + + +------------------------------------+--------+--------+--------+ + | | |aio| | |sync| | |sans| | + +====================================+========+========+========+ + | Connect to a TCP socket | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Connect to a Unix socket | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Connect using a preexisting socket | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Encrypt connection with TLS | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Close connection on context exit | ✅ | ✅ | — | + +------------------------------------+--------+--------+--------+ + | Reconnect automatically | ✅ | ❌ | — | + +------------------------------------+--------+--------+--------+ + | Configure ``Origin`` header | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Configure ``User-Agent`` header | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Alter opening handshake request | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Connect to non-ASCII IRIs | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Perform HTTP Basic Authentication | ✅ | ✅ | ✅ | + +------------------------------------+--------+--------+--------+ + | Perform HTTP Digest Authentication | ❌ | ❌ | ❌ | + | (`#784`_) | | | | + +------------------------------------+--------+--------+--------+ + | Follow HTTP redirects | ✅ | ❌ | — | + +------------------------------------+--------+--------+--------+ + | Connect via a HTTP proxy (`#364`_) | ❌ | ❌ | — | + +------------------------------------+--------+--------+--------+ + | Connect via a SOCKS5 proxy | ❌ | ❌ | — | + | (`#475`_) | | | | + +------------------------------------+--------+--------+--------+ + +.. _#364: https://github.com/python-websockets/websockets/issues/364 +.. _#475: https://github.com/python-websockets/websockets/issues/475 +.. _#784: https://github.com/python-websockets/websockets/issues/784 + +Known limitations +----------------- + +There is no way to control compression of outgoing frames on a per-frame basis +(`#538`_). If compression is enabled, all frames are compressed. + +.. _#538: https://github.com/python-websockets/websockets/issues/538 + +The server doesn't check the Host header and respond with a HTTP 400 Bad Request +if it is missing or invalid (`#1246`). + +.. _#1246: https://github.com/python-websockets/websockets/issues/1246 + +The client API doesn't attempt to guarantee that there is no more than one +connection to a given IP address in a CONNECTING state. This behavior is +`mandated by RFC 6455`_. However, :func:`~client.connect()` isn't the right +layer for enforcing this constraint. It's the caller's responsibility. + +.. _mandated by RFC 6455: https://www.rfc-editor.org/rfc/rfc6455.html#section-4.1 diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/reference/index.rst b/tests/wpt/tests/tools/third_party/websockets/docs/reference/index.rst new file mode 100644 index 00000000000..0b80f087a1f --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/reference/index.rst @@ -0,0 +1,90 @@ +API reference +============= + +.. currentmodule:: websockets + +Features +-------- + +Check which implementations support which features and known limitations. + +.. toctree:: + :titlesonly: + + features + + +:mod:`asyncio` +-------------- + +This is the default implementation. It's ideal for servers that handle many +clients concurrently. + +.. toctree:: + :titlesonly: + + asyncio/server + asyncio/client + +:mod:`threading` +---------------- + +This alternative implementation can be a good choice for clients. + +.. toctree:: + :titlesonly: + + sync/server + sync/client + +`Sans-I/O`_ +----------- + +This layer is designed for integrating in third-party libraries, typically +application servers. + +.. _Sans-I/O: https://sans-io.readthedocs.io/ + +.. toctree:: + :titlesonly: + + sansio/server + sansio/client + +Extensions +---------- + +The Per-Message Deflate extension is built in. You may also define custom +extensions. + +.. toctree:: + :titlesonly: + + extensions + +Shared +------ + +These low-level APIs are shared by all implementations. + +.. toctree:: + :titlesonly: + + datastructures + exceptions + types + +API stability +------------- + +Public APIs documented in this API reference are subject to the +:ref:`backwards-compatibility policy <backwards-compatibility policy>`. + +Anything that isn't listed in the API reference is a private API. There's no +guarantees of behavior or backwards-compatibility for private APIs. + +Convenience imports +------------------- + +For convenience, many public APIs can be imported directly from the +``websockets`` package. diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/reference/sansio/client.rst b/tests/wpt/tests/tools/third_party/websockets/docs/reference/sansio/client.rst new file mode 100644 index 00000000000..09bafc74558 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/reference/sansio/client.rst @@ -0,0 +1,58 @@ +Client (`Sans-I/O`_) +==================== + +.. _Sans-I/O: https://sans-io.readthedocs.io/ + +.. currentmodule:: websockets.client + +.. autoclass:: ClientProtocol(wsuri, origin=None, extensions=None, subprotocols=None, state=State.CONNECTING, max_size=2 ** 20, logger=None) + + .. automethod:: receive_data + + .. automethod:: receive_eof + + .. automethod:: connect + + .. automethod:: send_request + + .. automethod:: send_continuation + + .. automethod:: send_text + + .. automethod:: send_binary + + .. automethod:: send_close + + .. automethod:: send_ping + + .. automethod:: send_pong + + .. automethod:: fail + + .. automethod:: events_received + + .. automethod:: data_to_send + + .. automethod:: close_expected + + WebSocket protocol objects also provide these attributes: + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: state + + The following attributes are available after the opening handshake, + once the WebSocket connection is open: + + .. autoattribute:: handshake_exc + + The following attributes are available after the closing handshake, + once the WebSocket connection is closed: + + .. autoproperty:: close_code + + .. autoproperty:: close_reason + + .. autoproperty:: close_exc diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/reference/sansio/common.rst b/tests/wpt/tests/tools/third_party/websockets/docs/reference/sansio/common.rst new file mode 100644 index 00000000000..cd1ef3c63a5 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/reference/sansio/common.rst @@ -0,0 +1,64 @@ +:orphan: + +Both sides (`Sans-I/O`_) +========================= + +.. _Sans-I/O: https://sans-io.readthedocs.io/ + +.. automodule:: websockets.protocol + +.. autoclass:: Protocol(side, state=State.OPEN, max_size=2 ** 20, logger=None) + + .. automethod:: receive_data + + .. automethod:: receive_eof + + .. automethod:: send_continuation + + .. automethod:: send_text + + .. automethod:: send_binary + + .. automethod:: send_close + + .. automethod:: send_ping + + .. automethod:: send_pong + + .. automethod:: fail + + .. automethod:: events_received + + .. automethod:: data_to_send + + .. automethod:: close_expected + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: state + + .. autoproperty:: close_code + + .. autoproperty:: close_reason + + .. autoproperty:: close_exc + +.. autoclass:: Side + + .. autoattribute:: SERVER + + .. autoattribute:: CLIENT + +.. autoclass:: State + + .. autoattribute:: CONNECTING + + .. autoattribute:: OPEN + + .. autoattribute:: CLOSING + + .. autoattribute:: CLOSED + +.. autodata:: SEND_EOF diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/reference/sansio/server.rst b/tests/wpt/tests/tools/third_party/websockets/docs/reference/sansio/server.rst new file mode 100644 index 00000000000..d70df6277a4 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/reference/sansio/server.rst @@ -0,0 +1,62 @@ +Server (`Sans-I/O`_) +==================== + +.. _Sans-I/O: https://sans-io.readthedocs.io/ + +.. currentmodule:: websockets.server + +.. autoclass:: ServerProtocol(origins=None, extensions=None, subprotocols=None, state=State.CONNECTING, max_size=2 ** 20, logger=None) + + .. automethod:: receive_data + + .. automethod:: receive_eof + + .. automethod:: accept + + .. automethod:: select_subprotocol + + .. automethod:: reject + + .. automethod:: send_response + + .. automethod:: send_continuation + + .. automethod:: send_text + + .. automethod:: send_binary + + .. automethod:: send_close + + .. automethod:: send_ping + + .. automethod:: send_pong + + .. automethod:: fail + + .. automethod:: events_received + + .. automethod:: data_to_send + + .. automethod:: close_expected + + WebSocket protocol objects also provide these attributes: + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: state + + The following attributes are available after the opening handshake, + once the WebSocket connection is open: + + .. autoattribute:: handshake_exc + + The following attributes are available after the closing handshake, + once the WebSocket connection is closed: + + .. autoproperty:: close_code + + .. autoproperty:: close_reason + + .. autoproperty:: close_exc diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/reference/sync/client.rst b/tests/wpt/tests/tools/third_party/websockets/docs/reference/sync/client.rst new file mode 100644 index 00000000000..6cccd6ec486 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/reference/sync/client.rst @@ -0,0 +1,49 @@ +Client (:mod:`threading`) +========================= + +.. automodule:: websockets.sync.client + +Opening a connection +-------------------- + +.. autofunction:: connect(uri, *, sock=None, ssl_context=None, server_hostname=None, origin=None, extensions=None, subprotocols=None, additional_headers=None, user_agent_header="Python/x.y.z websockets/X.Y", compression="deflate", open_timeout=10, close_timeout=10, max_size=2 ** 20, logger=None, create_connection=None) + +.. autofunction:: unix_connect(path, uri=None, *, sock=None, ssl_context=None, server_hostname=None, origin=None, extensions=None, subprotocols=None, additional_headers=None, user_agent_header="Python/x.y.z websockets/X.Y", compression="deflate", open_timeout=10, close_timeout=10, max_size=2 ** 20, logger=None, create_connection=None) + +Using a connection +------------------ + +.. autoclass:: ClientConnection + + .. automethod:: __iter__ + + .. automethod:: recv + + .. automethod:: recv_streaming + + .. automethod:: send + + .. automethod:: close + + .. automethod:: ping + + .. automethod:: pong + + WebSocket connection objects also provide these attributes: + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: local_address + + .. autoproperty:: remote_address + + The following attributes are available after the opening handshake, + once the WebSocket connection is open: + + .. autoattribute:: request + + .. autoattribute:: response + + .. autoproperty:: subprotocol diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/reference/sync/common.rst b/tests/wpt/tests/tools/third_party/websockets/docs/reference/sync/common.rst new file mode 100644 index 00000000000..3dc6d4a5096 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/reference/sync/common.rst @@ -0,0 +1,41 @@ +:orphan: + +Both sides (:mod:`threading`) +============================= + +.. automodule:: websockets.sync.connection + +.. autoclass:: Connection + + .. automethod:: __iter__ + + .. automethod:: recv + + .. automethod:: recv_streaming + + .. automethod:: send + + .. automethod:: close + + .. automethod:: ping + + .. automethod:: pong + + WebSocket connection objects also provide these attributes: + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: local_address + + .. autoproperty:: remote_address + + The following attributes are available after the opening handshake, + once the WebSocket connection is open: + + .. autoattribute:: request + + .. autoattribute:: response + + .. autoproperty:: subprotocol diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/reference/sync/server.rst b/tests/wpt/tests/tools/third_party/websockets/docs/reference/sync/server.rst new file mode 100644 index 00000000000..35c112046ac --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/reference/sync/server.rst @@ -0,0 +1,60 @@ +Server (:mod:`threading`) +========================= + +.. automodule:: websockets.sync.server + +Creating a server +----------------- + +.. autofunction:: serve(handler, host=None, port=None, *, sock=None, ssl_context=None, origins=None, extensions=None, subprotocols=None, select_subprotocol=None, process_request=None, process_response=None, server_header="Python/x.y.z websockets/X.Y", compression="deflate", open_timeout=10, close_timeout=10, max_size=2 ** 20, logger=None, create_connection=None) + +.. autofunction:: unix_serve(handler, path=None, *, sock=None, ssl_context=None, origins=None, extensions=None, subprotocols=None, select_subprotocol=None, process_request=None, process_response=None, server_header="Python/x.y.z websockets/X.Y", compression="deflate", open_timeout=10, close_timeout=10, max_size=2 ** 20, logger=None, create_connection=None) + +Running a server +---------------- + +.. autoclass:: WebSocketServer + + .. automethod:: serve_forever + + .. automethod:: shutdown + + .. automethod:: fileno + +Using a connection +------------------ + +.. autoclass:: ServerConnection + + .. automethod:: __iter__ + + .. automethod:: recv + + .. automethod:: recv_streaming + + .. automethod:: send + + .. automethod:: close + + .. automethod:: ping + + .. automethod:: pong + + WebSocket connection objects also provide these attributes: + + .. autoattribute:: id + + .. autoattribute:: logger + + .. autoproperty:: local_address + + .. autoproperty:: remote_address + + The following attributes are available after the opening handshake, + once the WebSocket connection is open: + + .. autoattribute:: request + + .. autoattribute:: response + + .. autoproperty:: subprotocol diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/reference/types.rst b/tests/wpt/tests/tools/third_party/websockets/docs/reference/types.rst new file mode 100644 index 00000000000..9d3aa8310bc --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/reference/types.rst @@ -0,0 +1,24 @@ +Types +===== + +.. automodule:: websockets.typing + + .. autodata:: Data + + .. autodata:: LoggerLike + + .. autodata:: StatusLike + + .. autodata:: Origin + + .. autodata:: Subprotocol + + .. autodata:: ExtensionName + + .. autodata:: ExtensionParameter + +.. autodata:: websockets.protocol.Event + +.. autodata:: websockets.datastructures.HeadersLike + +.. autodata:: websockets.datastructures.SupportsKeysAndGetItem diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/requirements.txt b/tests/wpt/tests/tools/third_party/websockets/docs/requirements.txt new file mode 100644 index 00000000000..bcd1d711435 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/requirements.txt @@ -0,0 +1,8 @@ +furo +sphinx +sphinx-autobuild +sphinx-copybutton +sphinx-inline-tabs +sphinxcontrib-spelling +sphinxcontrib-trio +sphinxext-opengraph diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/spelling_wordlist.txt b/tests/wpt/tests/tools/third_party/websockets/docs/spelling_wordlist.txt new file mode 100644 index 00000000000..dfa7065e79e --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/spelling_wordlist.txt @@ -0,0 +1,85 @@ +augustin +auth +autoscaler +aymeric +backend +backoff +backpressure +balancer +balancers +bottlenecked +bufferbloat +bugfix +buildpack +bytestring +bytestrings +changelog +coroutine +coroutines +cryptocurrencies +cryptocurrency +css +ctrl +deserialize +django +dev +Dockerfile +dyno +formatter +fractalideas +gunicorn +healthz +html +hypercorn +iframe +IPv +istio +iterable +js +keepalive +KiB +kubernetes +lifecycle +linkerd +liveness +lookups +MiB +mutex +mypy +nginx +Paketo +permessage +pid +procfile +proxying +py +pythonic +reconnection +redis +redistributions +retransmit +runtime +scalable +stateful +subclasses +subclassing +submodule +subpackages +subprotocol +subprotocols +supervisord +tidelift +tls +tox +txt +unregister +uple +uvicorn +uvloop +virtualenv +WebSocket +websocket +websockets +ws +wsgi +www diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/topics/authentication.rst b/tests/wpt/tests/tools/third_party/websockets/docs/topics/authentication.rst new file mode 100644 index 00000000000..31bc8e6da8a --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/topics/authentication.rst @@ -0,0 +1,348 @@ +Authentication +============== + +The WebSocket protocol was designed for creating web applications that need +bidirectional communication between clients running in browsers and servers. + +In most practical use cases, WebSocket servers need to authenticate clients in +order to route communications appropriately and securely. + +:rfc:`6455` stays elusive when it comes to authentication: + + This protocol doesn't prescribe any particular way that servers can + authenticate clients during the WebSocket handshake. The WebSocket + server can use any client authentication mechanism available to a + generic HTTP server, such as cookies, HTTP authentication, or TLS + authentication. + +None of these three mechanisms works well in practice. Using cookies is +cumbersome, HTTP authentication isn't supported by all mainstream browsers, +and TLS authentication in a browser is an esoteric user experience. + +Fortunately, there are better alternatives! Let's discuss them. + +System design +------------- + +Consider a setup where the WebSocket server is separate from the HTTP server. + +Most servers built with websockets to complement a web application adopt this +design because websockets doesn't aim at supporting HTTP. + +The following diagram illustrates the authentication flow. + +.. image:: authentication.svg + +Assuming the current user is authenticated with the HTTP server (1), the +application needs to obtain credentials from the HTTP server (2) in order to +send them to the WebSocket server (3), who can check them against the database +of user accounts (4). + +Usernames and passwords aren't a good choice of credentials here, if only +because passwords aren't available in clear text in the database. + +Tokens linked to user accounts are a better choice. These tokens must be +impossible to forge by an attacker. For additional security, they can be +short-lived or even single-use. + +Sending credentials +------------------- + +Assume the web application obtained authentication credentials, likely a +token, from the HTTP server. There's four options for passing them to the +WebSocket server. + +1. **Sending credentials as the first message in the WebSocket connection.** + + This is fully reliable and the most secure mechanism in this discussion. It + has two minor downsides: + + * Authentication is performed at the application layer. Ideally, it would + be managed at the protocol layer. + + * Authentication is performed after the WebSocket handshake, making it + impossible to monitor authentication failures with HTTP response codes. + +2. **Adding credentials to the WebSocket URI in a query parameter.** + + This is also fully reliable but less secure. Indeed, it has a major + downside: + + * URIs end up in logs, which leaks credentials. Even if that risk could be + lowered with single-use tokens, it is usually considered unacceptable. + + Authentication is still performed at the application layer but it can + happen before the WebSocket handshake, which improves separation of + concerns and enables responding to authentication failures with HTTP 401. + +3. **Setting a cookie on the domain of the WebSocket URI.** + + Cookies are undoubtedly the most common and hardened mechanism for sending + credentials from a web application to a server. In an HTTP application, + credentials would be a session identifier or a serialized, signed session. + + Unfortunately, when the WebSocket server runs on a different domain from + the web application, this idea bumps into the `Same-Origin Policy`_. For + security reasons, setting a cookie on a different origin is impossible. + + The proper workaround consists in: + + * creating a hidden iframe_ served from the domain of the WebSocket server + * sending the token to the iframe with postMessage_ + * setting the cookie in the iframe + + before opening the WebSocket connection. + + Sharing a parent domain (e.g. example.com) between the HTTP server (e.g. + www.example.com) and the WebSocket server (e.g. ws.example.com) and setting + the cookie on that parent domain would work too. + + However, the cookie would be shared with all subdomains of the parent + domain. For a cookie containing credentials, this is unacceptable. + +.. _Same-Origin Policy: https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy +.. _iframe: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe +.. _postMessage: https://developer.mozilla.org/en-US/docs/Web/API/MessagePort/postMessage + +4. **Adding credentials to the WebSocket URI in user information.** + + Letting the browser perform HTTP Basic Auth is a nice idea in theory. + + In practice it doesn't work due to poor support in browsers. + + As of May 2021: + + * Chrome 90 behaves as expected. + + * Firefox 88 caches credentials too aggressively. + + When connecting again to the same server with new credentials, it reuses + the old credentials, which may be expired, resulting in an HTTP 401. Then + the next connection succeeds. Perhaps errors clear the cache. + + When tokens are short-lived or single-use, this bug produces an + interesting effect: every other WebSocket connection fails. + + * Safari 14 ignores credentials entirely. + +Two other options are off the table: + +1. **Setting a custom HTTP header** + + This would be the most elegant mechanism, solving all issues with the options + discussed above. + + Unfortunately, it doesn't work because the `WebSocket API`_ doesn't support + `setting custom headers`_. + +.. _WebSocket API: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API +.. _setting custom headers: https://github.com/whatwg/html/issues/3062 + +2. **Authenticating with a TLS certificate** + + While this is suggested by the RFC, installing a TLS certificate is too far + from the mainstream experience of browser users. This could make sense in + high security contexts. I hope developers working on such projects don't + take security advice from the documentation of random open source projects. + +Let's experiment! +----------------- + +The `experiments/authentication`_ directory demonstrates these techniques. + +Run the experiment in an environment where websockets is installed: + +.. _experiments/authentication: https://github.com/python-websockets/websockets/tree/main/experiments/authentication + +.. code-block:: console + + $ python experiments/authentication/app.py + Running on http://localhost:8000/ + +When you browse to the HTTP server at http://localhost:8000/ and you submit a +username, the server creates a token and returns a testing web page. + +This page opens WebSocket connections to four WebSocket servers running on +four different origins. It attempts to authenticate with the token in four +different ways. + +First message +............. + +As soon as the connection is open, the client sends a message containing the +token: + +.. code-block:: javascript + + const websocket = new WebSocket("ws://.../"); + websocket.onopen = () => websocket.send(token); + + // ... + +At the beginning of the connection handler, the server receives this message +and authenticates the user. If authentication fails, the server closes the +connection: + +.. code-block:: python + + async def first_message_handler(websocket): + token = await websocket.recv() + user = get_user(token) + if user is None: + await websocket.close(CloseCode.INTERNAL_ERROR, "authentication failed") + return + + ... + +Query parameter +............... + +The client adds the token to the WebSocket URI in a query parameter before +opening the connection: + +.. code-block:: javascript + + const uri = `ws://.../?token=${token}`; + const websocket = new WebSocket(uri); + + // ... + +The server intercepts the HTTP request, extracts the token and authenticates +the user. If authentication fails, it returns an HTTP 401: + +.. code-block:: python + + class QueryParamProtocol(websockets.WebSocketServerProtocol): + async def process_request(self, path, headers): + token = get_query_parameter(path, "token") + if token is None: + return http.HTTPStatus.UNAUTHORIZED, [], b"Missing token\n" + + user = get_user(token) + if user is None: + return http.HTTPStatus.UNAUTHORIZED, [], b"Invalid token\n" + + self.user = user + + async def query_param_handler(websocket): + user = websocket.user + + ... + +Cookie +...... + +The client sets a cookie containing the token before opening the connection. + +The cookie must be set by an iframe loaded from the same origin as the +WebSocket server. This requires passing the token to this iframe. + +.. code-block:: javascript + + // in main window + iframe.contentWindow.postMessage(token, "http://..."); + + // in iframe + document.cookie = `token=${data}; SameSite=Strict`; + + // in main window + const websocket = new WebSocket("ws://.../"); + + // ... + +This sequence must be synchronized between the main window and the iframe. +This involves several events. Look at the full implementation for details. + +The server intercepts the HTTP request, extracts the token and authenticates +the user. If authentication fails, it returns an HTTP 401: + +.. code-block:: python + + class CookieProtocol(websockets.WebSocketServerProtocol): + async def process_request(self, path, headers): + # Serve iframe on non-WebSocket requests + ... + + token = get_cookie(headers.get("Cookie", ""), "token") + if token is None: + return http.HTTPStatus.UNAUTHORIZED, [], b"Missing token\n" + + user = get_user(token) + if user is None: + return http.HTTPStatus.UNAUTHORIZED, [], b"Invalid token\n" + + self.user = user + + async def cookie_handler(websocket): + user = websocket.user + + ... + +User information +................ + +The client adds the token to the WebSocket URI in user information before +opening the connection: + +.. code-block:: javascript + + const uri = `ws://token:${token}@.../`; + const websocket = new WebSocket(uri); + + // ... + +Since HTTP Basic Auth is designed to accept a username and a password rather +than a token, we send ``token`` as username and the token as password. + +The server intercepts the HTTP request, extracts the token and authenticates +the user. If authentication fails, it returns an HTTP 401: + +.. code-block:: python + + class UserInfoProtocol(websockets.BasicAuthWebSocketServerProtocol): + async def check_credentials(self, username, password): + if username != "token": + return False + + user = get_user(password) + if user is None: + return False + + self.user = user + return True + + async def user_info_handler(websocket): + user = websocket.user + + ... + +Machine-to-machine authentication +--------------------------------- + +When the WebSocket client is a standalone program rather than a script running +in a browser, there are far fewer constraints. HTTP Authentication is the best +solution in this scenario. + +To authenticate a websockets client with HTTP Basic Authentication +(:rfc:`7617`), include the credentials in the URI: + +.. code-block:: python + + async with websockets.connect( + f"wss://{username}:{password}@example.com", + ) as websocket: + ... + +(You must :func:`~urllib.parse.quote` ``username`` and ``password`` if they +contain unsafe characters.) + +To authenticate a websockets client with HTTP Bearer Authentication +(:rfc:`6750`), add a suitable ``Authorization`` header: + +.. code-block:: python + + async with websockets.connect( + "wss://example.com", + extra_headers={"Authorization": f"Bearer {token}"} + ) as websocket: + ... diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/topics/authentication.svg b/tests/wpt/tests/tools/third_party/websockets/docs/topics/authentication.svg new file mode 100644 index 00000000000..ad2ad0e4428 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/topics/authentication.svg @@ -0,0 +1,63 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="-514 74 644 380" width="644px" height="380px"><defs><style>@font-face{ + font-family:"DIN Next"; + font-weight: 400; + src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix"); + src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/094b15e3-94bd-435b-a595-d40edfde661a.woff2") format("woff2"),url("https://whimsical.com/fonts/7e5fbe11-4858-4bd1-9ec6-a1d9f9d227aa.woff") format("woff"),url("https://whimsical.com/fonts/0f11eff9-9f05-46f5-9703-027c510065d7.ttf") format("truetype"),url("https://whimsical.com/fonts/48b61978-3f30-4274-823c-5cdcd1876918.svg#48b61978-3f30-4274-823c-5cdcd1876918") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 400; + font-style: italic; + src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix"); + src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/46251881-ffe9-4bfb-99c7-d6ce3bebaf3e.woff2") format("woff2"),url("https://whimsical.com/fonts/790ebbf2-62c5-4a32-946f-99d405f9243e.woff") format("woff"),url("https://whimsical.com/fonts/d28199e6-0f4a-42df-97f4-606701c6f75a.ttf") format("truetype"),url("https://whimsical.com/fonts/37a462c0-d86e-492c-b9ab-35e6bd417f6c.svg#37a462c0-d86e-492c-b9ab-35e6bd417f6c") format("svg"); +} +@font-face{ + font-weight: 500; + font-family:"DIN Next"; + src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix"); + src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/7b29ae40-30ff-4f99-a2b9-cde88669fa2f.woff2") format("woff2"),url("https://whimsical.com/fonts/bf73077c-e354-4562-a085-f4703eb1d653.woff") format("woff"),url("https://whimsical.com/fonts/0ffa6947-5317-4d07-b525-14d08a028821.ttf") format("truetype"),url("https://whimsical.com/fonts/9e423e45-5450-4991-a157-dbe6cf61eb4e.svg#9e423e45-5450-4991-a157-dbe6cf61eb4e") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 500; + font-style: italic; + src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix"); + src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/c7717981-647d-4b76-8817-33062e42d11f.woff2") format("woff2"),url("https://whimsical.com/fonts/b852cd4c-1255-40b1-a2be-73a9105b0155.woff") format("woff"),url("https://whimsical.com/fonts/821b00ad-e741-4e2d-af1a-85594367c8a2.ttf") format("truetype"),url("https://whimsical.com/fonts/d3e3b689-a6b0-45f2-b279-f5e194f87409.svg#d3e3b689-a6b0-45f2-b279-f5e194f87409") format("svg"); +} +@font-face{ + font-weight: 700; + font-family:"DIN Next"; + src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix"); + src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/31704504-4671-47a6-a61e-397f07410d91.woff2") format("woff2"),url("https://whimsical.com/fonts/b8a280da-481f-44a0-8d9c-1bc64bd7227c.woff") format("woff"),url("https://whimsical.com/fonts/276d122a-0fab-447b-9fc0-5d7fb0eafce2.ttf") format("truetype"),url("https://whimsical.com/fonts/8fb8273a-8213-4928-808b-b5bfaf3fd7e9.svg#8fb8273a-8213-4928-808b-b5bfaf3fd7e9") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 700; + font-style: italic; + src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix"); + src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/4132c4c8-680c-4d6d-9251-a2da38503bbd.woff2") format("woff2"),url("https://whimsical.com/fonts/366401fe-6df4-47be-8f55-8a411cff0dd2.woff") format("woff"),url("https://whimsical.com/fonts/dbe4f7ba-fc16-44a6-a577-571620e9edaf.ttf") format("truetype"),url("https://whimsical.com/fonts/f874edca-ee87-4ccf-8b1d-921fbc3c1c36.svg#f874edca-ee87-4ccf-8b1d-921fbc3c1c36") format("svg"); +} + +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Regular.woff') format('woff'); + font-weight: 400; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Italic.woff') format('woff'); + font-style: italic; + font-weight: 400; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Bold.woff') format('woff'); + font-weight: 700; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-BoldItalic.woff') format('woff'); + font-style: italic; + font-weight: 700; +} +* {-webkit-font-smoothing: auto; -moz-osx-font-smoothing: auto;}@media print { svg { width: 100%; height: 100%; } }</style><filter id="fill-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.16"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="fill-light-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.04"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="image-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="frame-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="badge-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.08"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g><g><rect x="-504" y="228" width="192" height="72" rx="3" ry="3" fill="#cfeeeb"/><rect y="228" rx="3" stroke="#1AAE9F" fill="none" stroke-linejoin="round" width="192" stroke-linecap="round" stroke-width="2" x="-504" ry="3" height="72"/><text y="240" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="168" xml:space="preserve" x="-492" font-family="DIN Next, sans-serif" height="48"><tspan x="-408" y="258" text-anchor="middle" style="white-space:pre;">HTTP</tspan><tspan x="-408" y="282" text-anchor="middle" style="white-space:pre;">server</tspan></text></g></g><g><g><rect x="-72" y="228" width="192" height="72" rx="3" ry="3" fill="#d3e6f7"/><rect y="228" rx="3" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="192" stroke-linecap="round" stroke-width="2" x="-72" ry="3" height="72"/><text y="240" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="168" xml:space="preserve" x="-60" font-family="DIN Next, sans-serif" height="48"><tspan x="24" y="258" text-anchor="middle" style="white-space:pre;">WebSocket</tspan><tspan x="24" y="282" text-anchor="middle" style="white-space:pre;">server</tspan></text></g></g><g><g><rect x="-288" y="84" width="192" height="72" rx="3" ry="3" fill="#d9dde0"/><rect y="84" rx="3" stroke="#4B5C6B" fill="none" stroke-linejoin="round" width="192" stroke-linecap="round" stroke-width="2" x="-288" ry="3" height="72"/><text y="96" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="168" xml:space="preserve" x="-276" font-family="DIN Next, sans-serif" height="48"><tspan x="-192" y="114" text-anchor="middle" style="white-space:pre;">web app</tspan><tspan x="-192" y="138" text-anchor="middle" style="white-space:pre;">in browser</tspan></text></g></g><g><g><path d="M-96,372c0,6.62741 -42.98074,12 -96,12c-53.01926,0 -96,-5.37259 -96,-12c0,-6.62741 42.98074,-12 96,-12c53.01926,0 96,5.37259 96,12c0,6.62741 0,53.37259 0,60c0,6.62741 -42.98074,12 -96,12c-53.01926,0 -96,-5.37259 -96,-12c0,-6.62741 0,-60 0,-60" fill="#e2cdf2"/><path d="M-96,372c0,6.62741 -42.98074,12 -96,12c-53.01926,0 -96,-5.37259 -96,-12c0,-6.62741 42.98074,-12 96,-12c53.01926,0 96,5.37259 96,12c0,6.62741 0,53.37259 0,60c0,6.62741 -42.98074,12 -96,12c-53.01926,0 -96,-5.37259 -96,-12c0,-6.62741 0,-60 0,-60" fill="none" stroke="#730FC3" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/><text y="396" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="168" xml:space="preserve" x="-276" font-family="DIN Next, sans-serif" height="24"><tspan x="-192" y="414" text-anchor="middle" style="white-space:pre;">user accounts</tspan></text></g></g><g><g><path d="M-400.46606,302.69069l37.26606,13.30931M-284.8,344l33.4991,11.96396" fill="none" stroke="#1AAE9F" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-252.34924,349.70583 -247.53394,357.30931 -256.07648,360.14213" fill="#1AAE9F" stroke="#1AAE9F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="318" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="160" xml:space="preserve" x="-404" font-family="DIN Next, sans-serif" height="24"><tspan x="-324" y="336" text-anchor="middle" style="white-space:pre;">(1) authenticate user</tspan></text></g></g><g><g><path d="M-247.35316,159.15135l-43.98017,18.84865M-356.66667,206l-40.30359,17.27297" fill="none" stroke="#1AAE9F" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-391.94549,227.14787 -400.64684,224.84865 -396.31086,216.96199" fill="#1AAE9F" stroke="#1AAE9F" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="180" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="164" xml:space="preserve" x="-406" font-family="DIN Next, sans-serif" height="24"><tspan x="-324" y="198" text-anchor="middle" style="white-space:pre;">(2) obtain credentials</tspan></text></g></g><g><g><path d="M-136.64684,159.15135l43.98017,18.84865M-27.33333,206l40.30359,17.27297" fill="none" stroke="#2C88D9" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="12.31086,216.96199 16.64684,224.84865 7.94549,227.14787" fill="#2C88D9" stroke="#2C88D9" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="180" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="153" xml:space="preserve" x="-136.5" font-family="DIN Next, sans-serif" height="24"><tspan x="-60" y="198" text-anchor="middle" style="white-space:pre;">(3) send credentials</tspan></text></g></g><g><g><path d="M16.46606,302.69069l-37.26606,13.30931M-99.2,344l-33.4991,11.96396" fill="none" stroke="#2C88D9" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-127.92352,360.14213 -136.46606,357.30931 -131.65076,349.70583" fill="#2C88D9" stroke="#2C88D9" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="318" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="160" xml:space="preserve" x="-140" font-family="DIN Next, sans-serif" height="24"><tspan x="-60" y="336" text-anchor="middle" style="white-space:pre;">(4) authenticate user</tspan></text></g></g></svg>
\ No newline at end of file diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/topics/broadcast.rst b/tests/wpt/tests/tools/third_party/websockets/docs/topics/broadcast.rst new file mode 100644 index 00000000000..1acb372d4f3 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/topics/broadcast.rst @@ -0,0 +1,348 @@ +Broadcasting messages +===================== + +.. currentmodule:: websockets + + +.. admonition:: If you just want to send a message to all connected clients, + use :func:`broadcast`. + :class: tip + + If you want to learn about its design in depth, continue reading this + document. + +WebSocket servers often send the same message to all connected clients or to a +subset of clients for which the message is relevant. + +Let's explore options for broadcasting a message, explain the design +of :func:`broadcast`, and discuss alternatives. + +For each option, we'll provide a connection handler called ``handler()`` and a +function or coroutine called ``broadcast()`` that sends a message to all +connected clients. + +Integrating them is left as an exercise for the reader. You could start with:: + + import asyncio + import websockets + + async def handler(websocket): + ... + + async def broadcast(message): + ... + + async def broadcast_messages(): + while True: + await asyncio.sleep(1) + message = ... # your application logic goes here + await broadcast(message) + + async def main(): + async with websockets.serve(handler, "localhost", 8765): + await broadcast_messages() # runs forever + + if __name__ == "__main__": + asyncio.run(main()) + +``broadcast_messages()`` must yield control to the event loop between each +message, or else it will never let the server run. That's why it includes +``await asyncio.sleep(1)``. + +A complete example is available in the `experiments/broadcast`_ directory. + +.. _experiments/broadcast: https://github.com/python-websockets/websockets/tree/main/experiments/broadcast + +The naive way +------------- + +The most obvious way to send a message to all connected clients consists in +keeping track of them and sending the message to each of them. + +Here's a connection handler that registers clients in a global variable:: + + CLIENTS = set() + + async def handler(websocket): + CLIENTS.add(websocket) + try: + await websocket.wait_closed() + finally: + CLIENTS.remove(websocket) + +This implementation assumes that the client will never send any messages. If +you'd rather not make this assumption, you can change:: + + await websocket.wait_closed() + +to:: + + async for _ in websocket: + pass + +Here's a coroutine that broadcasts a message to all clients:: + + async def broadcast(message): + for websocket in CLIENTS.copy(): + try: + await websocket.send(message) + except websockets.ConnectionClosed: + pass + +There are two tricks in this version of ``broadcast()``. + +First, it makes a copy of ``CLIENTS`` before iterating it. Else, if a client +connects or disconnects while ``broadcast()`` is running, the loop would fail +with:: + + RuntimeError: Set changed size during iteration + +Second, it ignores :exc:`~exceptions.ConnectionClosed` exceptions because a +client could disconnect between the moment ``broadcast()`` makes a copy of +``CLIENTS`` and the moment it sends a message to this client. This is fine: a +client that disconnected doesn't belongs to "all connected clients" anymore. + +The naive way can be very fast. Indeed, if all connections have enough free +space in their write buffers, ``await websocket.send(message)`` writes the +message and returns immediately, as it doesn't need to wait for the buffer to +drain. In this case, ``broadcast()`` doesn't yield control to the event loop, +which minimizes overhead. + +The naive way can also fail badly. If the write buffer of a connection reaches +``write_limit``, ``broadcast()`` waits for the buffer to drain before sending +the message to other clients. This can cause a massive drop in performance. + +As a consequence, this pattern works only when write buffers never fill up, +which is usually outside of the control of the server. + +If you know for sure that you will never write more than ``write_limit`` bytes +within ``ping_interval + ping_timeout``, then websockets will terminate slow +connections before the write buffer has time to fill up. + +Don't set extreme ``write_limit``, ``ping_interval``, and ``ping_timeout`` +values to ensure that this condition holds. Set reasonable values and use the +built-in :func:`broadcast` function instead. + +The concurrent way +------------------ + +The naive way didn't work well because it serialized writes, while the whole +point of asynchronous I/O is to perform I/O concurrently. + +Let's modify ``broadcast()`` to send messages concurrently:: + + async def send(websocket, message): + try: + await websocket.send(message) + except websockets.ConnectionClosed: + pass + + def broadcast(message): + for websocket in CLIENTS: + asyncio.create_task(send(websocket, message)) + +We move the error handling logic in a new coroutine and we schedule +a :class:`~asyncio.Task` to run it instead of executing it immediately. + +Since ``broadcast()`` no longer awaits coroutines, we can make it a function +rather than a coroutine and do away with the copy of ``CLIENTS``. + +This version of ``broadcast()`` makes clients independent from one another: a +slow client won't block others. As a side effect, it makes messages +independent from one another. + +If you broadcast several messages, there is no strong guarantee that they will +be sent in the expected order. Fortunately, the event loop runs tasks in the +order in which they are created, so the order is correct in practice. + +Technically, this is an implementation detail of the event loop. However, it +seems unlikely for an event loop to run tasks in an order other than FIFO. + +If you wanted to enforce the order without relying this implementation detail, +you could be tempted to wait until all clients have received the message:: + + async def broadcast(message): + if CLIENTS: # asyncio.wait doesn't accept an empty list + await asyncio.wait([ + asyncio.create_task(send(websocket, message)) + for websocket in CLIENTS + ]) + +However, this doesn't really work in practice. Quite often, it will block +until the slowest client times out. + +Backpressure meets broadcast +---------------------------- + +At this point, it becomes apparent that backpressure, usually a good practice, +doesn't work well when broadcasting a message to thousands of clients. + +When you're sending messages to a single client, you don't want to send them +faster than the network can transfer them and the client accept them. This is +why :meth:`~server.WebSocketServerProtocol.send` checks if the write buffer +is full and, if it is, waits until it drain, giving the network and the +client time to catch up. This provides backpressure. + +Without backpressure, you could pile up data in the write buffer until the +server process runs out of memory and the operating system kills it. + +The :meth:`~server.WebSocketServerProtocol.send` API is designed to enforce +backpressure by default. This helps users of websockets write robust programs +even if they never heard about backpressure. + +For comparison, :class:`asyncio.StreamWriter` requires users to understand +backpressure and to await :meth:`~asyncio.StreamWriter.drain` explicitly +after each :meth:`~asyncio.StreamWriter.write`. + +When broadcasting messages, backpressure consists in slowing down all clients +in an attempt to let the slowest client catch up. With thousands of clients, +the slowest one is probably timing out and isn't going to receive the message +anyway. So it doesn't make sense to synchronize with the slowest client. + +How do we avoid running out of memory when slow clients can't keep up with the +broadcast rate, then? The most straightforward option is to disconnect them. + +If a client gets too far behind, eventually it reaches the limit defined by +``ping_timeout`` and websockets terminates the connection. You can read the +discussion of :doc:`keepalive and timeouts <./timeouts>` for details. + +How :func:`broadcast` works +--------------------------- + +The built-in :func:`broadcast` function is similar to the naive way. The main +difference is that it doesn't apply backpressure. + +This provides the best performance by avoiding the overhead of scheduling and +running one task per client. + +Also, when sending text messages, encoding to UTF-8 happens only once rather +than once per client, providing a small performance gain. + +Per-client queues +----------------- + +At this point, we deal with slow clients rather brutally: we disconnect then. + +Can we do better? For example, we could decide to skip or to batch messages, +depending on how far behind a client is. + +To implement this logic, we can create a queue of messages for each client and +run a task that gets messages from the queue and sends them to the client:: + + import asyncio + + CLIENTS = set() + + async def relay(queue, websocket): + while True: + # Implement custom logic based on queue.qsize() and + # websocket.transport.get_write_buffer_size() here. + message = await queue.get() + await websocket.send(message) + + async def handler(websocket): + queue = asyncio.Queue() + relay_task = asyncio.create_task(relay(queue, websocket)) + CLIENTS.add(queue) + try: + await websocket.wait_closed() + finally: + CLIENTS.remove(queue) + relay_task.cancel() + +Then we can broadcast a message by pushing it to all queues:: + + def broadcast(message): + for queue in CLIENTS: + queue.put_nowait(message) + +The queues provide an additional buffer between the ``broadcast()`` function +and clients. This makes it easier to support slow clients without excessive +memory usage because queued messages aren't duplicated to write buffers +until ``relay()`` processes them. + +Publish–subscribe +----------------- + +Can we avoid centralizing the list of connected clients in a global variable? + +If each client subscribes to a stream a messages, then broadcasting becomes as +simple as publishing a message to the stream. + +Here's a message stream that supports multiple consumers:: + + class PubSub: + def __init__(self): + self.waiter = asyncio.Future() + + def publish(self, value): + waiter, self.waiter = self.waiter, asyncio.Future() + waiter.set_result((value, self.waiter)) + + async def subscribe(self): + waiter = self.waiter + while True: + value, waiter = await waiter + yield value + + __aiter__ = subscribe + + PUBSUB = PubSub() + +The stream is implemented as a linked list of futures. It isn't necessary to +synchronize consumers. They can read the stream at their own pace, +independently from one another. Once all consumers read a message, there are +no references left, therefore the garbage collector deletes it. + +The connection handler subscribes to the stream and sends messages:: + + async def handler(websocket): + async for message in PUBSUB: + await websocket.send(message) + +The broadcast function publishes to the stream:: + + def broadcast(message): + PUBSUB.publish(message) + +Like per-client queues, this version supports slow clients with limited memory +usage. Unlike per-client queues, it makes it difficult to tell how far behind +a client is. The ``PubSub`` class could be extended or refactored to provide +this information. + +The ``for`` loop is gone from this version of the ``broadcast()`` function. +However, there's still a ``for`` loop iterating on all clients hidden deep +inside :mod:`asyncio`. When ``publish()`` sets the result of the ``waiter`` +future, :mod:`asyncio` loops on callbacks registered with this future and +schedules them. This is how connection handlers receive the next value from +the asynchronous iterator returned by ``subscribe()``. + +Performance considerations +-------------------------- + +The built-in :func:`broadcast` function sends all messages without yielding +control to the event loop. So does the naive way when the network and clients +are fast and reliable. + +For each client, a WebSocket frame is prepared and sent to the network. This +is the minimum amount of work required to broadcast a message. + +It would be tempting to prepare a frame and reuse it for all connections. +However, this isn't possible in general for two reasons: + +* Clients can negotiate different extensions. You would have to enforce the + same extensions with the same parameters. For example, you would have to + select some compression settings and reject clients that cannot support + these settings. + +* Extensions can be stateful, producing different encodings of the same + message depending on previous messages. For example, you would have to + disable context takeover to make compression stateless, resulting in poor + compression rates. + +All other patterns discussed above yield control to the event loop once per +client because messages are sent by different tasks. This makes them slower +than the built-in :func:`broadcast` function. + +There is no major difference between the performance of per-client queues and +publish–subscribe. diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/topics/compression.rst b/tests/wpt/tests/tools/third_party/websockets/docs/topics/compression.rst new file mode 100644 index 00000000000..eaf99070db0 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/topics/compression.rst @@ -0,0 +1,222 @@ +Compression +=========== + +.. currentmodule:: websockets.extensions.permessage_deflate + +Most WebSocket servers exchange JSON messages because they're convenient to +parse and serialize in a browser. These messages contain text data and tend to +be repetitive. + +This makes the stream of messages highly compressible. Enabling compression +can reduce network traffic by more than 80%. + +There's a standard for compressing messages. :rfc:`7692` defines WebSocket +Per-Message Deflate, a compression extension based on the Deflate_ algorithm. + +.. _Deflate: https://en.wikipedia.org/wiki/Deflate + +Configuring compression +----------------------- + +:func:`~websockets.client.connect` and :func:`~websockets.server.serve` enable +compression by default because the reduction in network bandwidth is usually +worth the additional memory and CPU cost. + +If you want to disable compression, set ``compression=None``:: + + import websockets + + websockets.connect(..., compression=None) + + websockets.serve(..., compression=None) + +If you want to customize compression settings, you can enable the Per-Message +Deflate extension explicitly with :class:`ClientPerMessageDeflateFactory` or +:class:`ServerPerMessageDeflateFactory`:: + + import websockets + from websockets.extensions import permessage_deflate + + websockets.connect( + ..., + extensions=[ + permessage_deflate.ClientPerMessageDeflateFactory( + server_max_window_bits=11, + client_max_window_bits=11, + compress_settings={"memLevel": 4}, + ), + ], + ) + + websockets.serve( + ..., + extensions=[ + permessage_deflate.ServerPerMessageDeflateFactory( + server_max_window_bits=11, + client_max_window_bits=11, + compress_settings={"memLevel": 4}, + ), + ], + ) + +The Window Bits and Memory Level values in these examples reduce memory usage +at the expense of compression rate. + +Compression settings +-------------------- + +When a client and a server enable the Per-Message Deflate extension, they +negotiate two parameters to guarantee compatibility between compression and +decompression. These parameters affect the trade-off between compression rate +and memory usage for both sides. + +* **Context Takeover** means that the compression context is retained between + messages. In other words, compression is applied to the stream of messages + rather than to each message individually. + + Context takeover should remain enabled to get good performance on + applications that send a stream of messages with similar structure, + that is, most applications. + + This requires retaining the compression context and state between messages, + which increases the memory footprint of a connection. + +* **Window Bits** controls the size of the compression context. It must be + an integer between 9 (lowest memory usage) and 15 (best compression). + Setting it to 8 is possible but rejected by some versions of zlib. + + On the server side, websockets defaults to 12. Specifically, the compression + window size (server to client) is always 12 while the decompression window + (client to server) size may be 12 or 15 depending on whether the client + supports configuring it. + + On the client side, websockets lets the server pick a suitable value, which + has the same effect as defaulting to 15. + +:mod:`zlib` offers additional parameters for tuning compression. They control +the trade-off between compression rate, memory usage, and CPU usage only for +compressing. They're transparent for decompressing. Unless mentioned +otherwise, websockets inherits defaults of :func:`~zlib.compressobj`. + +* **Memory Level** controls the size of the compression state. It must be an + integer between 1 (lowest memory usage) and 9 (best compression). + + websockets defaults to 5. This is lower than zlib's default of 8. Not only + does a lower memory level reduce memory usage, but it can also increase + speed thanks to memory locality. + +* **Compression Level** controls the effort to optimize compression. It must + be an integer between 1 (lowest CPU usage) and 9 (best compression). + +* **Strategy** selects the compression strategy. The best choice depends on + the type of data being compressed. + + +Tuning compression +------------------ + +For servers +........... + +By default, websockets enables compression with conservative settings that +optimize memory usage at the cost of a slightly worse compression rate: +Window Bits = 12 and Memory Level = 5. This strikes a good balance for small +messages that are typical of WebSocket servers. + +Here's how various compression settings affect memory usage of a single +connection on a 64-bit system, as well a benchmark of compressed size and +compression time for a corpus of small JSON documents. + +=========== ============ ============ ================ ================ +Window Bits Memory Level Memory usage Size vs. default Time vs. default +=========== ============ ============ ================ ================ +15 8 322 KiB -4.0% +15% +14 7 178 KiB -2.6% +10% +13 6 106 KiB -1.4% +5% +**12** **5** **70 KiB** **=** **=** +11 4 52 KiB +3.7% -5% +10 3 43 KiB +90% +50% +9 2 39 KiB +160% +100% +— — 19 KiB +452% — +=========== ============ ============ ================ ================ + +Window Bits and Memory Level don't have to move in lockstep. However, other +combinations don't yield significantly better results than those shown above. + +Compressed size and compression time depend heavily on the kind of messages +exchanged by the application so this example may not apply to your use case. + +You can adapt `compression/benchmark.py`_ by creating a list of typical +messages and passing it to the ``_run`` function. + +Window Bits = 11 and Memory Level = 4 looks like the sweet spot in this table. + +websockets defaults to Window Bits = 12 and Memory Level = 5 to stay away from +Window Bits = 10 or Memory Level = 3 where performance craters, raising doubts +on what could happen at Window Bits = 11 and Memory Level = 4 on a different +corpus. + +Defaults must be safe for all applications, hence a more conservative choice. + +.. _compression/benchmark.py: https://github.com/python-websockets/websockets/blob/main/experiments/compression/benchmark.py + +The benchmark focuses on compression because it's more expensive than +decompression. Indeed, leaving aside small allocations, theoretical memory +usage is: + +* ``(1 << (windowBits + 2)) + (1 << (memLevel + 9))`` for compression; +* ``1 << windowBits`` for decompression. + +CPU usage is also higher for compression than decompression. + +While it's always possible for a server to use a smaller window size for +compressing outgoing messages, using a smaller window size for decompressing +incoming messages requires collaboration from clients. + +When a client doesn't support configuring the size of its compression window, +websockets enables compression with the largest possible decompression window. +In most use cases, this is more efficient than disabling compression both ways. + +If you are very sensitive to memory usage, you can reverse this behavior by +setting the ``require_client_max_window_bits`` parameter of +:class:`ServerPerMessageDeflateFactory` to ``True``. + +For clients +........... + +By default, websockets enables compression with Memory Level = 5 but leaves +the Window Bits setting up to the server. + +There's two good reasons and one bad reason for not optimizing the client side +like the server side: + +1. If the maintainers of a server configured some optimized settings, we don't + want to override them with more restrictive settings. + +2. Optimizing memory usage doesn't matter very much for clients because it's + uncommon to open thousands of client connections in a program. + +3. On a more pragmatic note, some servers misbehave badly when a client + configures compression settings. `AWS API Gateway`_ is the worst offender. + + .. _AWS API Gateway: https://github.com/python-websockets/websockets/issues/1065 + + Unfortunately, even though websockets is right and AWS is wrong, many users + jump to the conclusion that websockets doesn't work. + + Until the ecosystem levels up, interoperability with buggy servers seems + more valuable than optimizing memory usage. + + +Further reading +--------------- + +This `blog post by Ilya Grigorik`_ provides more details about how compression +settings affect memory usage and how to optimize them. + +.. _blog post by Ilya Grigorik: https://www.igvita.com/2013/11/27/configuring-and-optimizing-websocket-compression/ + +This `experiment by Peter Thorson`_ recommends Window Bits = 11 and Memory +Level = 4 for optimizing memory usage. + +.. _experiment by Peter Thorson: https://mailarchive.ietf.org/arch/msg/hybi/F9t4uPufVEy8KBLuL36cZjCmM_Y/ diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/topics/data-flow.svg b/tests/wpt/tests/tools/third_party/websockets/docs/topics/data-flow.svg new file mode 100644 index 00000000000..749d9d482d2 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/topics/data-flow.svg @@ -0,0 +1,63 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="-382 -214 500 464" width="500px" height="464px"><defs><style>@font-face{ + font-family:"DIN Next"; + font-weight: 400; + src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix"); + src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/094b15e3-94bd-435b-a595-d40edfde661a.woff2") format("woff2"),url("https://whimsical.com/fonts/7e5fbe11-4858-4bd1-9ec6-a1d9f9d227aa.woff") format("woff"),url("https://whimsical.com/fonts/0f11eff9-9f05-46f5-9703-027c510065d7.ttf") format("truetype"),url("https://whimsical.com/fonts/48b61978-3f30-4274-823c-5cdcd1876918.svg#48b61978-3f30-4274-823c-5cdcd1876918") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 400; + font-style: italic; + src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix"); + src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/46251881-ffe9-4bfb-99c7-d6ce3bebaf3e.woff2") format("woff2"),url("https://whimsical.com/fonts/790ebbf2-62c5-4a32-946f-99d405f9243e.woff") format("woff"),url("https://whimsical.com/fonts/d28199e6-0f4a-42df-97f4-606701c6f75a.ttf") format("truetype"),url("https://whimsical.com/fonts/37a462c0-d86e-492c-b9ab-35e6bd417f6c.svg#37a462c0-d86e-492c-b9ab-35e6bd417f6c") format("svg"); +} +@font-face{ + font-weight: 500; + font-family:"DIN Next"; + src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix"); + src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/7b29ae40-30ff-4f99-a2b9-cde88669fa2f.woff2") format("woff2"),url("https://whimsical.com/fonts/bf73077c-e354-4562-a085-f4703eb1d653.woff") format("woff"),url("https://whimsical.com/fonts/0ffa6947-5317-4d07-b525-14d08a028821.ttf") format("truetype"),url("https://whimsical.com/fonts/9e423e45-5450-4991-a157-dbe6cf61eb4e.svg#9e423e45-5450-4991-a157-dbe6cf61eb4e") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 500; + font-style: italic; + src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix"); + src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/c7717981-647d-4b76-8817-33062e42d11f.woff2") format("woff2"),url("https://whimsical.com/fonts/b852cd4c-1255-40b1-a2be-73a9105b0155.woff") format("woff"),url("https://whimsical.com/fonts/821b00ad-e741-4e2d-af1a-85594367c8a2.ttf") format("truetype"),url("https://whimsical.com/fonts/d3e3b689-a6b0-45f2-b279-f5e194f87409.svg#d3e3b689-a6b0-45f2-b279-f5e194f87409") format("svg"); +} +@font-face{ + font-weight: 700; + font-family:"DIN Next"; + src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix"); + src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/31704504-4671-47a6-a61e-397f07410d91.woff2") format("woff2"),url("https://whimsical.com/fonts/b8a280da-481f-44a0-8d9c-1bc64bd7227c.woff") format("woff"),url("https://whimsical.com/fonts/276d122a-0fab-447b-9fc0-5d7fb0eafce2.ttf") format("truetype"),url("https://whimsical.com/fonts/8fb8273a-8213-4928-808b-b5bfaf3fd7e9.svg#8fb8273a-8213-4928-808b-b5bfaf3fd7e9") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 700; + font-style: italic; + src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix"); + src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/4132c4c8-680c-4d6d-9251-a2da38503bbd.woff2") format("woff2"),url("https://whimsical.com/fonts/366401fe-6df4-47be-8f55-8a411cff0dd2.woff") format("woff"),url("https://whimsical.com/fonts/dbe4f7ba-fc16-44a6-a577-571620e9edaf.ttf") format("truetype"),url("https://whimsical.com/fonts/f874edca-ee87-4ccf-8b1d-921fbc3c1c36.svg#f874edca-ee87-4ccf-8b1d-921fbc3c1c36") format("svg"); +} + +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Regular.woff') format('woff'); + font-weight: 400; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Italic.woff') format('woff'); + font-style: italic; + font-weight: 400; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Bold.woff') format('woff'); + font-weight: 700; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-BoldItalic.woff') format('woff'); + font-style: italic; + font-weight: 700; +} +* {-webkit-font-smoothing: auto; -moz-osx-font-smoothing: auto;}@media print { svg { width: 100%; height: 100%; } }</style><filter id="fill-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.16"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="fill-light-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.04"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="image-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="frame-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="badge-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.08"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g><g><rect x="-372" y="-12" width="480" height="72" rx="3" ry="3" fill="#f6d8dd"/><rect y="-12" rx="3" stroke="#D3455B" fill="none" stroke-linejoin="round" width="480" stroke-linecap="round" stroke-width="2" x="-372" ry="3" height="72"/><text y="12" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="456" xml:space="preserve" x="-360" font-family="DIN Next, sans-serif" height="24"><tspan x="-132" y="30" text-anchor="middle" style="white-space:pre;">Integration layer</tspan></text></g></g><g><g><rect x="-372" y="168" width="480" height="72" rx="3" ry="3" fill="#d3e6f7"/><rect y="168" rx="3" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="480" stroke-linecap="round" stroke-width="2" x="-372" ry="3" height="72"/><text y="192" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="456" xml:space="preserve" x="-360" font-family="DIN Next, sans-serif" height="24"><tspan x="-132" y="210" text-anchor="middle" style="white-space:pre;">Sans-I/O layer</tspan></text></g></g><g><g><rect x="-372" y="-192" width="216" height="72" rx="3" ry="3" fill="#cfeeeb"/><rect y="-192" rx="3" stroke="#1AAE9F" fill="none" stroke-linejoin="round" width="216" stroke-linecap="round" stroke-width="2" x="-372" ry="3" height="72"/><text y="-168" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="192" xml:space="preserve" x="-360" font-family="DIN Next, sans-serif" height="24"><tspan x="-264" y="-150" text-anchor="middle" style="white-space:pre;">Application</tspan></text></g></g><g><g><path d="M-324,-20v-20M-324,-92v-16" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-329.54095,-104.9079 -324,-112 -318.45905,-104.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="-90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="79" xml:space="preserve" x="-363.5" font-family="DIN Next, sans-serif" height="48"><tspan x="-324" y="-72" text-anchor="middle" style="white-space:pre;">receive</tspan><tspan x="-324" y="-48" text-anchor="middle" style="white-space:pre;">messages</tspan></text></g></g><g><g><path d="M-204,-112l0,20M-204,-40v16" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-198.45905,-27.0921 -204,-20 -209.54095,-27.0921" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="-90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="79" xml:space="preserve" x="-243.5" font-family="DIN Next, sans-serif" height="48"><tspan x="-204" y="-72" text-anchor="middle" style="white-space:pre;">send</tspan><tspan x="-204" y="-48" text-anchor="middle" style="white-space:pre;">messages</tspan></text></g></g><g><g><path d="M96.94345,-173.14258c4.53106,1.57532 8.08271,4.56716 9.84726,8.29515c1.76456,3.72799 1.59205,7.87526 -0.4783,11.4987c1.926,5.62898 0.07743,11.66356 -4.87418,15.91158c-4.95161,4.24802 -12.28578,6.09138 -19.33826,4.86046c-2.37232,7.09867 -9.03221,12.73751 -17.68526,14.97387c-8.65305,2.23636 -18.11659,0.76458 -25.13032,-3.90829c-8.39761,8.37233 -20.93327,13.315 -34.25257,13.5054c-13.3193,0.1904 -26.06267,-4.39089 -34.82012,-12.51799c-15.68591,7.18472 -35.58915,2.88148 -44.76245,-9.678c-7.38426,1.34031 -15.11657,-0.22307 -20.85552,-4.21672c-5.73895,-3.99365 -8.80965,-9.94793 -8.28226,-16.05983c-3.457,-2.81303 -4.97104,-6.82861 -4.04383,-10.72516c0.92721,-3.89655 4.17556,-7.16931 8.67598,-8.74119c1.08624,-6.50166 5.74572,-12.25693 12.67613,-15.65724c6.93042,-3.40031 15.38838,-4.08092 23.00992,-1.85161c7.11534,-8.80068 18.56556,-14.69575 31.42744,-16.1802c12.86188,-1.48445 25.89062,1.58538 35.76002,8.42577c6.64655,-4.34187 15.16482,-6.34654 23.65054,-5.56587c8.48572,0.78068 16.23125,4.28161 21.50507,9.72014c19.34894,-5.64384 40.71077,2.33211 47.97072,17.91102z" fill="#e3e6e9"/><path d="M96.94345,-173.14258c4.53106,1.57532 8.08271,4.56716 9.84726,8.29515c1.76456,3.72799 1.59205,7.87526 -0.4783,11.4987c1.926,5.62898 0.07743,11.66356 -4.87418,15.91158c-4.95161,4.24802 -12.28578,6.09138 -19.33826,4.86046c-2.37232,7.09867 -9.03221,12.73751 -17.68526,14.97387c-8.65305,2.23636 -18.11659,0.76458 -25.13032,-3.90829c-8.39761,8.37233 -20.93327,13.315 -34.25257,13.5054c-13.3193,0.1904 -26.06267,-4.39089 -34.82012,-12.51799c-15.68591,7.18472 -35.58915,2.88148 -44.76245,-9.678c-7.38426,1.34031 -15.11657,-0.22307 -20.85552,-4.21672c-5.73895,-3.99365 -8.80965,-9.94793 -8.28226,-16.05983c-3.457,-2.81303 -4.97104,-6.82861 -4.04383,-10.72516c0.92721,-3.89655 4.17556,-7.16931 8.67598,-8.74119c1.08624,-6.50166 5.74572,-12.25693 12.67613,-15.65724c6.93042,-3.40031 15.38838,-4.08092 23.00992,-1.85161c7.11534,-8.80068 18.56556,-14.69575 31.42744,-16.1802c12.86188,-1.48445 25.89062,1.58538 35.76002,8.42577c6.64655,-4.34187 15.16482,-6.34654 23.65054,-5.56587c8.48572,0.78068 16.23125,4.28161 21.50507,9.72014c19.34894,-5.64384 40.71077,2.33211 47.97072,17.91102z" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/><text y="-168" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="192" xml:space="preserve" x="-96" font-family="DIN Next, sans-serif" height="24"><tspan x="0" y="-150" text-anchor="middle" style="white-space:pre;">Network</tspan></text></g></g><g><g><path d="M-60,-20v-20M-60,-92v-15.53727" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-65.54095,-104.44518 -60,-111.53727 -54.45905,-104.44518" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="-90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="38" xml:space="preserve" x="-79" font-family="DIN Next, sans-serif" height="48"><tspan x="-60" y="-72" text-anchor="middle" style="white-space:pre;">send</tspan><tspan x="-60" y="-48" text-anchor="middle" style="white-space:pre;">data</tspan></text></g></g><g><g><path d="M60,-108.78722v18.78722M60,-38v14" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="65.54095,-27.0921 60,-20 54.45905,-27.0921" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="-88" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="56" xml:space="preserve" x="32" font-family="DIN Next, sans-serif" height="48"><tspan x="60" y="-70" text-anchor="middle" style="white-space:pre;">receive</tspan><tspan x="60" y="-46" text-anchor="middle" style="white-space:pre;">data</tspan></text></g></g><g><g><path d="M60,68v20M60,140v16" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="65.54095,152.9079 60,160 54.45905,152.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="56" xml:space="preserve" x="32" font-family="DIN Next, sans-serif" height="48"><tspan x="60" y="108" text-anchor="middle" style="white-space:pre;">receive</tspan><tspan x="60" y="132" text-anchor="middle" style="white-space:pre;">bytes</tspan></text></g></g><g><g><path d="M-60,160v-20M-60,88v-16" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-65.54095,75.0921 -60,68 -54.45905,75.0921" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="42" xml:space="preserve" x="-81" font-family="DIN Next, sans-serif" height="48"><tspan x="-60" y="108" text-anchor="middle" style="white-space:pre;">send</tspan><tspan x="-60" y="132" text-anchor="middle" style="white-space:pre;">bytes</tspan></text></g></g><g><g><path d="M-212,42" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-212,42 -212,42 -212,42" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></g><g><g><path d="M-204,68v20M-204,140v16" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-198.45905,152.9079 -204,160 -209.54095,152.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="90" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="52" xml:space="preserve" x="-230" font-family="DIN Next, sans-serif" height="48"><tspan x="-204" y="108" text-anchor="middle" style="white-space:pre;">send</tspan><tspan x="-204" y="132" text-anchor="middle" style="white-space:pre;">events</tspan></text></g></g><g><g><path d="M-324,160l0,-18M-324,90v-14" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="4"/><polygon points="-329.54095,79.0921 -324,72 -318.45905,79.0921" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><text y="92" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="56" xml:space="preserve" x="-352" font-family="DIN Next, sans-serif" height="48"><tspan x="-324" y="110" text-anchor="middle" style="white-space:pre;">receive</tspan><tspan x="-324" y="134" text-anchor="middle" style="white-space:pre;">events</tspan></text></g></g></svg>
\ No newline at end of file diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/topics/deployment.rst b/tests/wpt/tests/tools/third_party/websockets/docs/topics/deployment.rst new file mode 100644 index 00000000000..2a1fe9a7850 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/topics/deployment.rst @@ -0,0 +1,181 @@ +Deployment +========== + +.. currentmodule:: websockets + +When you deploy your websockets server to production, at a high level, your +architecture will almost certainly look like the following diagram: + +.. image:: deployment.svg + +The basic unit for scaling a websockets server is "one server process". Each +blue box in the diagram represents one server process. + +There's more variation in routing. While the routing layer is shown as one big +box, it is likely to involve several subsystems. + +When you design a deployment, your should consider two questions: + +1. How will I run the appropriate number of server processes? +2. How will I route incoming connections to these processes? + +These questions are strongly related. There's a wide range of acceptable +answers, depending on your goals and your constraints. + +You can find a few concrete examples in the :ref:`deployment how-to guides +<deployment-howto>`. + +Running server processes +------------------------ + +How many processes do I need? +............................. + +Typically, one server process will manage a few hundreds or thousands +connections, depending on the frequency of messages and the amount of work +they require. + +CPU and memory usage increase with the number of connections to the server. + +Often CPU is the limiting factor. If a server process goes to 100% CPU, then +you reached the limit. How much headroom you want to keep is up to you. + +Once you know how many connections a server process can manage and how many +connections you need to handle, you can calculate how many processes to run. + +You can also automate this calculation by configuring an autoscaler to keep +CPU usage or connection count within acceptable limits. + +Don't scale with threads. Threads doesn't make sense for a server built with +:mod:`asyncio`. + +How do I run processes? +....................... + +Most solutions for running multiple instances of a server process fall into +one of these three buckets: + +1. Running N processes on a platform: + + * a Kubernetes Deployment + + * its equivalent on a Platform as a Service provider + +2. Running N servers: + + * an AWS Auto Scaling group, a GCP Managed instance group, etc. + + * a fixed set of long-lived servers + +3. Running N processes on a server: + + * preferably via a process manager or supervisor + +Option 1 is easiest of you have access to such a platform. + +Option 2 almost always combines with option 3. + +How do I start a process? +......................... + +Run a Python program that invokes :func:`~server.serve`. That's it. + +Don't run an ASGI server such as Uvicorn, Hypercorn, or Daphne. They're +alternatives to websockets, not complements. + +Don't run a WSGI server such as Gunicorn, Waitress, or mod_wsgi. They aren't +designed to run WebSocket applications. + +Applications servers handle network connections and expose a Python API. You +don't need one because websockets handles network connections directly. + +How do I stop a process? +........................ + +Process managers send the SIGTERM signal to terminate processes. Catch this +signal and exit the server to ensure a graceful shutdown. + +Here's an example: + +.. literalinclude:: ../../example/faq/shutdown_server.py + :emphasize-lines: 12-15,18 + +When exiting the context manager, :func:`~server.serve` closes all connections +with code 1001 (going away). As a consequence: + +* If the connection handler is awaiting + :meth:`~server.WebSocketServerProtocol.recv`, it receives a + :exc:`~exceptions.ConnectionClosedOK` exception. It can catch the exception + and clean up before exiting. + +* Otherwise, it should be waiting on + :meth:`~server.WebSocketServerProtocol.wait_closed`, so it can receive the + :exc:`~exceptions.ConnectionClosedOK` exception and exit. + +This example is easily adapted to handle other signals. + +If you override the default signal handler for SIGINT, which raises +:exc:`KeyboardInterrupt`, be aware that you won't be able to interrupt a +program with Ctrl-C anymore when it's stuck in a loop. + +Routing connections +------------------- + +What does routing involve? +.......................... + +Since the routing layer is directly exposed to the Internet, it should provide +appropriate protection against threats ranging from Internet background noise +to targeted attacks. + +You should always secure WebSocket connections with TLS. Since the routing +layer carries the public domain name, it should terminate TLS connections. + +Finally, it must route connections to the server processes, balancing new +connections across them. + +How do I route connections? +........................... + +Here are typical solutions for load balancing, matched to ways of running +processes: + +1. If you're running on a platform, it comes with a routing layer: + + * a Kubernetes Ingress and Service + + * a service mesh: Istio, Consul, Linkerd, etc. + + * the routing mesh of a Platform as a Service + +2. If you're running N servers, you may load balance with: + + * a cloud load balancer: AWS Elastic Load Balancing, GCP Cloud Load + Balancing, etc. + + * A software load balancer: HAProxy, NGINX, etc. + +3. If you're running N processes on a server, you may load balance with: + + * A software load balancer: HAProxy, NGINX, etc. + + * The operating system — all processes listen on the same port + +You may trust the load balancer to handle encryption and to provide security. +You may add another layer in front of the load balancer for these purposes. + +There are many possibilities. Don't add layers that you don't need, though. + +How do I implement a health check? +.................................. + +Load balancers need a way to check whether server processes are up and running +to avoid routing connections to a non-functional backend. + +websockets provide minimal support for responding to HTTP requests with the +:meth:`~server.WebSocketServerProtocol.process_request` hook. + +Here's an example: + +.. literalinclude:: ../../example/faq/health_check_server.py + :emphasize-lines: 7-9,18 diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/topics/deployment.svg b/tests/wpt/tests/tools/third_party/websockets/docs/topics/deployment.svg new file mode 100644 index 00000000000..fbacb18c4b7 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/topics/deployment.svg @@ -0,0 +1,63 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="-490 -34 644 356" width="644px" height="356px"><defs><style>@font-face{ + font-family:"DIN Next"; + font-weight: 400; + src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix"); + src:url("https://whimsical.com/fonts/139e8e25-0eea-4cb4-a5d4-79048803e73d.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/094b15e3-94bd-435b-a595-d40edfde661a.woff2") format("woff2"),url("https://whimsical.com/fonts/7e5fbe11-4858-4bd1-9ec6-a1d9f9d227aa.woff") format("woff"),url("https://whimsical.com/fonts/0f11eff9-9f05-46f5-9703-027c510065d7.ttf") format("truetype"),url("https://whimsical.com/fonts/48b61978-3f30-4274-823c-5cdcd1876918.svg#48b61978-3f30-4274-823c-5cdcd1876918") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 400; + font-style: italic; + src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix"); + src:url("https://whimsical.com/fonts/df24d5e8-5087-42fd-99b1-b16042d666c8.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/46251881-ffe9-4bfb-99c7-d6ce3bebaf3e.woff2") format("woff2"),url("https://whimsical.com/fonts/790ebbf2-62c5-4a32-946f-99d405f9243e.woff") format("woff"),url("https://whimsical.com/fonts/d28199e6-0f4a-42df-97f4-606701c6f75a.ttf") format("truetype"),url("https://whimsical.com/fonts/37a462c0-d86e-492c-b9ab-35e6bd417f6c.svg#37a462c0-d86e-492c-b9ab-35e6bd417f6c") format("svg"); +} +@font-face{ + font-weight: 500; + font-family:"DIN Next"; + src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix"); + src:url("https://whimsical.com/fonts/c5b058fc-55ce-4e06-a175-5c7d9a7e90f4.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/7b29ae40-30ff-4f99-a2b9-cde88669fa2f.woff2") format("woff2"),url("https://whimsical.com/fonts/bf73077c-e354-4562-a085-f4703eb1d653.woff") format("woff"),url("https://whimsical.com/fonts/0ffa6947-5317-4d07-b525-14d08a028821.ttf") format("truetype"),url("https://whimsical.com/fonts/9e423e45-5450-4991-a157-dbe6cf61eb4e.svg#9e423e45-5450-4991-a157-dbe6cf61eb4e") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 500; + font-style: italic; + src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix"); + src:url("https://whimsical.com/fonts/9897c008-fd65-48a4-afc7-36de2fea97b9.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/c7717981-647d-4b76-8817-33062e42d11f.woff2") format("woff2"),url("https://whimsical.com/fonts/b852cd4c-1255-40b1-a2be-73a9105b0155.woff") format("woff"),url("https://whimsical.com/fonts/821b00ad-e741-4e2d-af1a-85594367c8a2.ttf") format("truetype"),url("https://whimsical.com/fonts/d3e3b689-a6b0-45f2-b279-f5e194f87409.svg#d3e3b689-a6b0-45f2-b279-f5e194f87409") format("svg"); +} +@font-face{ + font-weight: 700; + font-family:"DIN Next"; + src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix"); + src:url("https://whimsical.com/fonts/81cd3b08-fd39-4ae3-8d05-9d24709eba84.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/31704504-4671-47a6-a61e-397f07410d91.woff2") format("woff2"),url("https://whimsical.com/fonts/b8a280da-481f-44a0-8d9c-1bc64bd7227c.woff") format("woff"),url("https://whimsical.com/fonts/276d122a-0fab-447b-9fc0-5d7fb0eafce2.ttf") format("truetype"),url("https://whimsical.com/fonts/8fb8273a-8213-4928-808b-b5bfaf3fd7e9.svg#8fb8273a-8213-4928-808b-b5bfaf3fd7e9") format("svg"); +} +@font-face{ + font-family:"DIN Next"; + font-weight: 700; + font-style: italic; + src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix"); + src:url("https://whimsical.com/fonts/00cc6df3-ed32-4004-8dd8-1c576600a408.eot?#iefix") format("eot"),url("https://whimsical.com/fonts/4132c4c8-680c-4d6d-9251-a2da38503bbd.woff2") format("woff2"),url("https://whimsical.com/fonts/366401fe-6df4-47be-8f55-8a411cff0dd2.woff") format("woff"),url("https://whimsical.com/fonts/dbe4f7ba-fc16-44a6-a577-571620e9edaf.ttf") format("truetype"),url("https://whimsical.com/fonts/f874edca-ee87-4ccf-8b1d-921fbc3c1c36.svg#f874edca-ee87-4ccf-8b1d-921fbc3c1c36") format("svg"); +} + +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Regular.woff') format('woff'); + font-weight: 400; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Italic.woff') format('woff'); + font-style: italic; + font-weight: 400; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-Bold.woff') format('woff'); + font-weight: 700; +} +@font-face { + font-family: 'PFDINMonoPro'; + src:url('https://whimsical.com/fonts/PFDINMonoPro-BoldItalic.woff') format('woff'); + font-style: italic; + font-weight: 700; +} +* {-webkit-font-smoothing: auto; -moz-osx-font-smoothing: auto;}@media print { svg { width: 100%; height: 100%; } }</style><filter id="fill-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.16"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="fill-light-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.04"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="image-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="frame-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="8"/><feOffset result="offsetblur" dx="0" dy="3"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.06"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter><filter id="badge-shadow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur in="SourceAlpha" stdDeviation="6"/><feOffset result="offsetblur" dx="0" dy="2"/><feComponentTransfer result="s0"><feFuncA type="linear" slope="0.08"/></feComponentTransfer><feMerge><feMergeNode in="s0"/><feMergeNode in="SourceGraphic"/></feMerge></filter></defs><g><g><path d="M132.40052,16.91125c8.20504,1.34238 13.19097,6.77278 11.13766,12.13055c-0.76086,3.6926 -4.09828,6.94687 -9.03771,8.81254c-4.93944,1.86567 -10.9238,2.13232 -16.20563,0.72209c-3.50792,6.79759 -12.12825,11.93728 -22.81409,13.60245c-10.68585,1.66516 -21.93194,-0.37877 -29.7633,-5.40937c-7.5447,11.32261 -23.72737,19.17936 -42.50646,20.63705c-18.77909,1.45769 -37.33189,-3.70278 -48.732,-13.55484c-11.00862,8.39145 -26.81112,13.46368 -43.71109,14.03023c-16.89997,0.56655 -33.41499,-3.42228 -45.68197,-11.03344c-12.09168,9.57066 -30.08677,15.12494 -49.08843,15.1514c-19.00166,0.02646 -37.03287,-5.47764 -49.18697,-15.01453c-32.22671,18.71922 -81.52408,17.01732 -110.54782,-3.81645c-9.61896,5.63808 -22.48701,8.30277 -35.32441,7.3149c-12.8374,-0.98787 -24.40407,-5.53286 -31.75198,-12.47659c-8.38039,4.12685 -19.07889,5.69428 -29.35634,4.30095c-10.27744,-1.39333 -19.13592,-5.61211 -24.30736,-11.5762c-6.03705,3.01428 -14.12058,3.62226 -21.04917,1.58316c-6.92858,-2.0391 -11.58448,-6.39632 -12.12376,-11.34603c-4.11078,-3.88947 -2.69127,-9.21224 3.19106,-11.96556c2.8597,-6.26181 10.13013,-11.25433 19.56423,-13.4345c9.43409,-2.18018 19.89581,-1.28549 28.15172,2.40755c4.56946,-5.60994 12.84561,-9.53066 22.43123,-10.62652c9.58562,-1.09585 19.40964,0.75561 26.62651,5.0181c13.21632,-10.89584 32.7134,-17.76363 53.91728,-18.99222c21.20388,-1.22858 42.24851,3.29017 58.19687,12.49616c11.75754,-9.7851 29.59785,-15.62692 48.64043,-15.92732c19.04258,-0.30041 37.29416,4.97204 49.76173,14.37498c12.30654,-11.80391 32.97339,-18.70238 54.83436,-18.30339c21.86097,0.39899 41.90535,8.04051 53.18277,20.27486c10.58585,-9.80304 28.36041,-15.18816 46.66019,-14.13654c18.29978,1.05162 34.36046,8.38113 42.16106,19.24077c7.63845,-6.15855 19.75125,-9.16677 31.72723,-7.87948c11.97598,1.2873 21.97263,6.67206 26.18437,14.10438c7.68618,-3.10406 17.06471,-3.86266 25.67861,-2.07706c8.6139,1.78561 15.60478,5.93747 19.14117,11.3679z" fill="#e3e6e9"/><path d="M132.40052,16.91125c8.20504,1.34238 13.19097,6.77278 11.13766,12.13055c-0.76086,3.6926 -4.09828,6.94687 -9.03771,8.81254c-4.93944,1.86567 -10.9238,2.13232 -16.20563,0.72209c-3.50792,6.79759 -12.12825,11.93728 -22.81409,13.60245c-10.68585,1.66516 -21.93194,-0.37877 -29.7633,-5.40937c-7.5447,11.32261 -23.72737,19.17936 -42.50646,20.63705c-18.77909,1.45769 -37.33189,-3.70278 -48.732,-13.55484c-11.00862,8.39145 -26.81112,13.46368 -43.71109,14.03023c-16.89997,0.56655 -33.41499,-3.42228 -45.68197,-11.03344c-12.09168,9.57066 -30.08677,15.12494 -49.08843,15.1514c-19.00166,0.02646 -37.03287,-5.47764 -49.18697,-15.01453c-32.22671,18.71922 -81.52408,17.01732 -110.54782,-3.81645c-9.61896,5.63808 -22.48701,8.30277 -35.32441,7.3149c-12.8374,-0.98787 -24.40407,-5.53286 -31.75198,-12.47659c-8.38039,4.12685 -19.07889,5.69428 -29.35634,4.30095c-10.27744,-1.39333 -19.13592,-5.61211 -24.30736,-11.5762c-6.03705,3.01428 -14.12058,3.62226 -21.04917,1.58316c-6.92858,-2.0391 -11.58448,-6.39632 -12.12376,-11.34603c-4.11078,-3.88947 -2.69127,-9.21224 3.19106,-11.96556c2.8597,-6.26181 10.13013,-11.25433 19.56423,-13.4345c9.43409,-2.18018 19.89581,-1.28549 28.15172,2.40755c4.56946,-5.60994 12.84561,-9.53066 22.43123,-10.62652c9.58562,-1.09585 19.40964,0.75561 26.62651,5.0181c13.21632,-10.89584 32.7134,-17.76363 53.91728,-18.99222c21.20388,-1.22858 42.24851,3.29017 58.19687,12.49616c11.75754,-9.7851 29.59785,-15.62692 48.64043,-15.92732c19.04258,-0.30041 37.29416,4.97204 49.76173,14.37498c12.30654,-11.80391 32.97339,-18.70238 54.83436,-18.30339c21.86097,0.39899 41.90535,8.04051 53.18277,20.27486c10.58585,-9.80304 28.36041,-15.18816 46.66019,-14.13654c18.29978,1.05162 34.36046,8.38113 42.16106,19.24077c7.63845,-6.15855 19.75125,-9.16677 31.72723,-7.87948c11.97598,1.2873 21.97263,6.67206 26.18437,14.10438c7.68618,-3.10406 17.06471,-3.86266 25.67861,-2.07706c8.6139,1.78561 15.60478,5.93747 19.14117,11.3679z" fill="none" stroke="#788896" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/><text y="12" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="600" xml:space="preserve" x="-468" font-family="DIN Next, sans-serif" height="24"><tspan x="-168" y="30" text-anchor="middle" style="white-space:pre;">Internet</tspan></text></g></g><g><g><rect x="-480" y="240" width="144" height="72" rx="3" ry="3" fill="#d3e6f7"/><rect y="240" rx="3" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="144" stroke-linecap="round" stroke-width="2" x="-480" ry="3" height="72"/><text y="264" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="120" xml:space="preserve" x="-468" font-family="DIN Next, sans-serif" height="24"><tspan x="-408" y="282" text-anchor="middle" style="white-space:pre;">websockets</tspan></text></g></g><g><g><rect x="-312" y="240" width="144" height="72" rx="3" ry="3" fill="#d3e6f7"/><rect y="240" rx="3" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="144" stroke-linecap="round" stroke-width="2" x="-312" ry="3" height="72"/><text y="264" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="120" xml:space="preserve" x="-300" font-family="DIN Next, sans-serif" height="24"><tspan x="-240" y="282" text-anchor="middle" style="white-space:pre;">websockets</tspan></text></g></g><g><g><rect x="0" y="240" width="144" height="72" rx="3" ry="3" fill="#d3e6f7"/><rect y="240" rx="3" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="144" stroke-linecap="round" stroke-width="2" x="0" ry="3" height="72"/><text y="264" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="120" xml:space="preserve" x="12" font-family="DIN Next, sans-serif" height="24"><tspan x="72" y="282" text-anchor="middle" style="white-space:pre;">websockets</tspan></text></g></g><g><g><rect x="-144" y="258" width="24" height="36" rx="1.5" ry="1.5" fill="#d3e6f7"/><rect y="258" rx="1.5" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="24" stroke-linecap="round" stroke-width="2" x="-144" ry="1.5" height="36"/></g></g><g><g><rect x="-48" y="258" width="24" height="36" rx="1.5" ry="1.5" fill="#d3e6f7"/><rect y="258" rx="1.5" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="24" stroke-linecap="round" stroke-width="2" x="-48" ry="1.5" height="36"/></g></g><g><g><rect x="-96" y="258" width="24" height="36" rx="1.5" ry="1.5" fill="#d3e6f7"/><rect y="258" rx="1.5" stroke="#2C88D9" fill="none" stroke-linejoin="round" width="24" stroke-linecap="round" stroke-width="2" x="-96" ry="1.5" height="36"/></g></g><g><g><rect x="-480" y="120" width="624" height="72" rx="3" ry="3" fill="#cfeeeb"/><rect y="120" rx="3" stroke="#1AAE9F" fill="none" stroke-linejoin="round" width="624" stroke-linecap="round" stroke-width="2" x="-480" ry="3" height="72"/><text y="144" font-style="normal" font-size="18" font-weight="normal" fill="#293845" width="600" xml:space="preserve" x="-468" font-family="DIN Next, sans-serif" height="24"><tspan x="-168" y="162" text-anchor="middle" style="white-space:pre;">routing</tspan></text></g></g><g><g><line stroke="#788896" fill="none" stroke-linejoin="round" y1="199.99999999999997" stroke-linecap="round" stroke-width="4" x1="-240" y2="227.99999999999994" x2="-240"/><polygon points="-234.45905,224.9079 -240,232 -245.54095,224.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></g><g><g><line stroke="#788896" fill="none" stroke-linejoin="round" y1="199.99999999999997" stroke-linecap="round" stroke-width="4" x1="-407.99999999999994" y2="227.99999999999994" x2="-407.99999999999994"/><polygon points="-402.45905,224.9079 -408,232 -413.54095,224.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></g><g><g><line stroke="#788896" fill="none" stroke-linejoin="round" y1="200.00000000000003" stroke-linecap="round" stroke-width="4" x1="72" y2="228" x2="72"/><polygon points="77.54095,224.9079 72,232 66.45905,224.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></g><g><g><line stroke="#788896" fill="none" stroke-linejoin="round" y1="79.92376615249583" stroke-linecap="round" stroke-width="4" x1="-168" y2="107.99999999999994" x2="-168"/><polygon points="-162.45905,104.9079 -168,112 -173.54095,104.9079" fill="#788896" stroke="#788896" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></g></svg>
\ No newline at end of file diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/topics/design.rst b/tests/wpt/tests/tools/third_party/websockets/docs/topics/design.rst new file mode 100644 index 00000000000..f164d29905e --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/topics/design.rst @@ -0,0 +1,572 @@ +Design +====== + +.. currentmodule:: websockets + +This document describes the design of websockets. It assumes familiarity with +the specification of the WebSocket protocol in :rfc:`6455`. + +It's primarily intended at maintainers. It may also be useful for users who +wish to understand what happens under the hood. + +.. warning:: + + Internals described in this document may change at any time. + + Backwards compatibility is only guaranteed for :doc:`public APIs + <../reference/index>`. + +Lifecycle +--------- + +State +..... + +WebSocket connections go through a trivial state machine: + +- ``CONNECTING``: initial state, +- ``OPEN``: when the opening handshake is complete, +- ``CLOSING``: when the closing handshake is started, +- ``CLOSED``: when the TCP connection is closed. + +Transitions happen in the following places: + +- ``CONNECTING -> OPEN``: in + :meth:`~legacy.protocol.WebSocketCommonProtocol.connection_open` which runs when + the :ref:`opening handshake <opening-handshake>` completes and the WebSocket + connection is established — not to be confused with + :meth:`~asyncio.BaseProtocol.connection_made` which runs when the TCP connection + is established; +- ``OPEN -> CLOSING``: in + :meth:`~legacy.protocol.WebSocketCommonProtocol.write_frame` immediately before + sending a close frame; since receiving a close frame triggers sending a + close frame, this does the right thing regardless of which side started the + :ref:`closing handshake <closing-handshake>`; also in + :meth:`~legacy.protocol.WebSocketCommonProtocol.fail_connection` which duplicates + a few lines of code from ``write_close_frame()`` and ``write_frame()``; +- ``* -> CLOSED``: in + :meth:`~legacy.protocol.WebSocketCommonProtocol.connection_lost` which is always + called exactly once when the TCP connection is closed. + +Coroutines +.......... + +The following diagram shows which coroutines are running at each stage of the +connection lifecycle on the client side. + +.. image:: lifecycle.svg + :target: _images/lifecycle.svg + +The lifecycle is identical on the server side, except inversion of control +makes the equivalent of :meth:`~client.connect` implicit. + +Coroutines shown in green are called by the application. Multiple coroutines +may interact with the WebSocket connection concurrently. + +Coroutines shown in gray manage the connection. When the opening handshake +succeeds, :meth:`~legacy.protocol.WebSocketCommonProtocol.connection_open` starts +two tasks: + +- :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` runs + :meth:`~legacy.protocol.WebSocketCommonProtocol.transfer_data` which handles + incoming data and lets :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` + consume it. It may be canceled to terminate the connection. It never exits + with an exception other than :exc:`~asyncio.CancelledError`. See :ref:`data + transfer <data-transfer>` below. + +- :attr:`~legacy.protocol.WebSocketCommonProtocol.keepalive_ping_task` runs + :meth:`~legacy.protocol.WebSocketCommonProtocol.keepalive_ping` which sends Ping + frames at regular intervals and ensures that corresponding Pong frames are + received. It is canceled when the connection terminates. It never exits + with an exception other than :exc:`~asyncio.CancelledError`. + +- :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` runs + :meth:`~legacy.protocol.WebSocketCommonProtocol.close_connection` which waits for + the data transfer to terminate, then takes care of closing the TCP + connection. It must not be canceled. It never exits with an exception. See + :ref:`connection termination <connection-termination>` below. + +Besides, :meth:`~legacy.protocol.WebSocketCommonProtocol.fail_connection` starts +the same :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` when +the opening handshake fails, in order to close the TCP connection. + +Splitting the responsibilities between two tasks makes it easier to guarantee +that websockets can terminate connections: + +- within a fixed timeout, +- without leaking pending tasks, +- without leaking open TCP connections, + +regardless of whether the connection terminates normally or abnormally. + +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` completes when no +more data will be received on the connection. Under normal circumstances, it +exits after exchanging close frames. + +:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` completes when +the TCP connection is closed. + + +.. _opening-handshake: + +Opening handshake +----------------- + +websockets performs the opening handshake when establishing a WebSocket +connection. On the client side, :meth:`~client.connect` executes it +before returning the protocol to the caller. On the server side, it's executed +before passing the protocol to the ``ws_handler`` coroutine handling the +connection. + +While the opening handshake is asymmetrical — the client sends an HTTP Upgrade +request and the server replies with an HTTP Switching Protocols response — +websockets aims at keeping the implementation of both sides consistent with +one another. + +On the client side, :meth:`~client.WebSocketClientProtocol.handshake`: + +- builds an HTTP request based on the ``uri`` and parameters passed to + :meth:`~client.connect`; +- writes the HTTP request to the network; +- reads an HTTP response from the network; +- checks the HTTP response, validates ``extensions`` and ``subprotocol``, and + configures the protocol accordingly; +- moves to the ``OPEN`` state. + +On the server side, :meth:`~server.WebSocketServerProtocol.handshake`: + +- reads an HTTP request from the network; +- calls :meth:`~server.WebSocketServerProtocol.process_request` which may + abort the WebSocket handshake and return an HTTP response instead; this + hook only makes sense on the server side; +- checks the HTTP request, negotiates ``extensions`` and ``subprotocol``, and + configures the protocol accordingly; +- builds an HTTP response based on the above and parameters passed to + :meth:`~server.serve`; +- writes the HTTP response to the network; +- moves to the ``OPEN`` state; +- returns the ``path`` part of the ``uri``. + +The most significant asymmetry between the two sides of the opening handshake +lies in the negotiation of extensions and, to a lesser extent, of the +subprotocol. The server knows everything about both sides and decides what the +parameters should be for the connection. The client merely applies them. + +If anything goes wrong during the opening handshake, websockets :ref:`fails +the connection <connection-failure>`. + + +.. _data-transfer: + +Data transfer +------------- + +Symmetry +........ + +Once the opening handshake has completed, the WebSocket protocol enters the +data transfer phase. This part is almost symmetrical. There are only two +differences between a server and a client: + +- `client-to-server masking`_: the client masks outgoing frames; the server + unmasks incoming frames; +- `closing the TCP connection`_: the server closes the connection immediately; + the client waits for the server to do it. + +.. _client-to-server masking: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.3 +.. _closing the TCP connection: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.1 + +These differences are so minor that all the logic for `data framing`_, for +`sending and receiving data`_ and for `closing the connection`_ is implemented +in the same class, :class:`~legacy.protocol.WebSocketCommonProtocol`. + +.. _data framing: https://www.rfc-editor.org/rfc/rfc6455.html#section-5 +.. _sending and receiving data: https://www.rfc-editor.org/rfc/rfc6455.html#section-6 +.. _closing the connection: https://www.rfc-editor.org/rfc/rfc6455.html#section-7 + +The :attr:`~legacy.protocol.WebSocketCommonProtocol.is_client` attribute tells which +side a protocol instance is managing. This attribute is defined on the +:attr:`~server.WebSocketServerProtocol` and +:attr:`~client.WebSocketClientProtocol` classes. + +Data flow +......... + +The following diagram shows how data flows between an application built on top +of websockets and a remote endpoint. It applies regardless of which side is +the server or the client. + +.. image:: protocol.svg + :target: _images/protocol.svg + +Public methods are shown in green, private methods in yellow, and buffers in +orange. Methods related to connection termination are omitted; connection +termination is discussed in another section below. + +Receiving data +.............. + +The left side of the diagram shows how websockets receives data. + +Incoming data is written to a :class:`~asyncio.StreamReader` in order to +implement flow control and provide backpressure on the TCP connection. + +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`, which is started +when the WebSocket connection is established, processes this data. + +When it receives data frames, it reassembles fragments and puts the resulting +messages in the :attr:`~legacy.protocol.WebSocketCommonProtocol.messages` queue. + +When it encounters a control frame: + +- if it's a close frame, it starts the closing handshake; +- if it's a ping frame, it answers with a pong frame; +- if it's a pong frame, it acknowledges the corresponding ping (unless it's an + unsolicited pong). + +Running this process in a task guarantees that control frames are processed +promptly. Without such a task, websockets would depend on the application to +drive the connection by having exactly one coroutine awaiting +:meth:`~legacy.protocol.WebSocketCommonProtocol.recv` at any time. While this +happens naturally in many use cases, it cannot be relied upon. + +Then :meth:`~legacy.protocol.WebSocketCommonProtocol.recv` fetches the next message +from the :attr:`~legacy.protocol.WebSocketCommonProtocol.messages` queue, with some +complexity added for handling backpressure and termination correctly. + +Sending data +............ + +The right side of the diagram shows how websockets sends data. + +:meth:`~legacy.protocol.WebSocketCommonProtocol.send` writes one or several data +frames containing the message. While sending a fragmented message, concurrent +calls to :meth:`~legacy.protocol.WebSocketCommonProtocol.send` are put on hold until +all fragments are sent. This makes concurrent calls safe. + +:meth:`~legacy.protocol.WebSocketCommonProtocol.ping` writes a ping frame and +yields a :class:`~asyncio.Future` which will be completed when a matching pong +frame is received. + +:meth:`~legacy.protocol.WebSocketCommonProtocol.pong` writes a pong frame. + +:meth:`~legacy.protocol.WebSocketCommonProtocol.close` writes a close frame and +waits for the TCP connection to terminate. + +Outgoing data is written to a :class:`~asyncio.StreamWriter` in order to +implement flow control and provide backpressure from the TCP connection. + +.. _closing-handshake: + +Closing handshake +................. + +When the other side of the connection initiates the closing handshake, +:meth:`~legacy.protocol.WebSocketCommonProtocol.read_message` receives a close +frame while in the ``OPEN`` state. It moves to the ``CLOSING`` state, sends a +close frame, and returns :obj:`None`, causing +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` to terminate. + +When this side of the connection initiates the closing handshake with +:meth:`~legacy.protocol.WebSocketCommonProtocol.close`, it moves to the ``CLOSING`` +state and sends a close frame. When the other side sends a close frame, +:meth:`~legacy.protocol.WebSocketCommonProtocol.read_message` receives it in the +``CLOSING`` state and returns :obj:`None`, also causing +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` to terminate. + +If the other side doesn't send a close frame within the connection's close +timeout, websockets :ref:`fails the connection <connection-failure>`. + +The closing handshake can take up to ``2 * close_timeout``: one +``close_timeout`` to write a close frame and one ``close_timeout`` to receive +a close frame. + +Then websockets terminates the TCP connection. + + +.. _connection-termination: + +Connection termination +---------------------- + +:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task`, which is +started when the WebSocket connection is established, is responsible for +eventually closing the TCP connection. + +First :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` waits +for :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` to terminate, +which may happen as a result of: + +- a successful closing handshake: as explained above, this exits the infinite + loop in :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`; +- a timeout while waiting for the closing handshake to complete: this cancels + :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`; +- a protocol error, including connection errors: depending on the exception, + :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` :ref:`fails the + connection <connection-failure>` with a suitable code and exits. + +:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` is separate +from :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` to make it +easier to implement the timeout on the closing handshake. Canceling +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` creates no risk +of canceling :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` +and failing to close the TCP connection, thus leaking resources. + +Then :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` cancels +:meth:`~legacy.protocol.WebSocketCommonProtocol.keepalive_ping`. This task has no +protocol compliance responsibilities. Terminating it to avoid leaking it is +the only concern. + +Terminating the TCP connection can take up to ``2 * close_timeout`` on the +server side and ``3 * close_timeout`` on the client side. Clients start by +waiting for the server to close the connection, hence the extra +``close_timeout``. Then both sides go through the following steps until the +TCP connection is lost: half-closing the connection (only for non-TLS +connections), closing the connection, aborting the connection. At this point +the connection drops regardless of what happens on the network. + + +.. _connection-failure: + +Connection failure +------------------ + +If the opening handshake doesn't complete successfully, websockets fails the +connection by closing the TCP connection. + +Once the opening handshake has completed, websockets fails the connection by +canceling :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` +and sending a close frame if appropriate. + +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` exits, unblocking +:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task`, which closes +the TCP connection. + + +.. _server-shutdown: + +Server shutdown +--------------- + +:class:`~websockets.server.WebSocketServer` closes asynchronously like +:class:`asyncio.Server`. The shutdown happen in two steps: + +1. Stop listening and accepting new connections; +2. Close established connections with close code 1001 (going away) or, if + the opening handshake is still in progress, with HTTP status code 503 + (Service Unavailable). + +The first call to :class:`~websockets.server.WebSocketServer.close` starts a +task that performs this sequence. Further calls are ignored. This is the +easiest way to make :class:`~websockets.server.WebSocketServer.close` and +:class:`~websockets.server.WebSocketServer.wait_closed` idempotent. + + +.. _cancellation: + +Cancellation +------------ + +User code +......... + +websockets provides a WebSocket application server. It manages connections and +passes them to user-provided connection handlers. This is an *inversion of +control* scenario: library code calls user code. + +If a connection drops, the corresponding handler should terminate. If the +server shuts down, all connection handlers must terminate. Canceling +connection handlers would terminate them. + +However, using cancellation for this purpose would require all connection +handlers to handle it properly. For example, if a connection handler starts +some tasks, it should catch :exc:`~asyncio.CancelledError`, terminate or +cancel these tasks, and then re-raise the exception. + +Cancellation is tricky in :mod:`asyncio` applications, especially when it +interacts with finalization logic. In the example above, what if a handler +gets interrupted with :exc:`~asyncio.CancelledError` while it's finalizing +the tasks it started, after detecting that the connection dropped? + +websockets considers that cancellation may only be triggered by the caller of +a coroutine when it doesn't care about the results of that coroutine anymore. +(Source: `Guido van Rossum <https://groups.google.com/forum/#!msg +/python-tulip/LZQe38CR3bg/7qZ1p_q5yycJ>`_). Since connection handlers run +arbitrary user code, websockets has no way of deciding whether that code is +still doing something worth caring about. + +For these reasons, websockets never cancels connection handlers. Instead it +expects them to detect when the connection is closed, execute finalization +logic if needed, and exit. + +Conversely, cancellation isn't a concern for WebSocket clients because they +don't involve inversion of control. + +Library +....... + +Most :doc:`public APIs <../reference/index>` of websockets are coroutines. +They may be canceled, for example if the user starts a task that calls these +coroutines and cancels the task later. websockets must handle this situation. + +Cancellation during the opening handshake is handled like any other exception: +the TCP connection is closed and the exception is re-raised. This can only +happen on the client side. On the server side, the opening handshake is +managed by websockets and nothing results in a cancellation. + +Once the WebSocket connection is established, internal tasks +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` and +:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` mustn't get +accidentally canceled if a coroutine that awaits them is canceled. In other +words, they must be shielded from cancellation. + +:meth:`~legacy.protocol.WebSocketCommonProtocol.recv` waits for the next message in +the queue or for :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` +to terminate, whichever comes first. It relies on :func:`~asyncio.wait` for +waiting on two futures in parallel. As a consequence, even though it's waiting +on a :class:`~asyncio.Future` signaling the next message and on +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`, it doesn't +propagate cancellation to them. + +:meth:`~legacy.protocol.WebSocketCommonProtocol.ensure_open` is called by +:meth:`~legacy.protocol.WebSocketCommonProtocol.send`, +:meth:`~legacy.protocol.WebSocketCommonProtocol.ping`, and +:meth:`~legacy.protocol.WebSocketCommonProtocol.pong`. When the connection state is +``CLOSING``, it waits for +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` but shields it to +prevent cancellation. + +:meth:`~legacy.protocol.WebSocketCommonProtocol.close` waits for the data transfer +task to terminate with :func:`~asyncio.timeout`. If it's canceled or if the +timeout elapses, :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` +is canceled, which is correct at this point. +:meth:`~legacy.protocol.WebSocketCommonProtocol.close` then waits for +:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` but shields it +to prevent cancellation. + +:meth:`~legacy.protocol.WebSocketCommonProtocol.close` and +:meth:`~legacy.protocol.WebSocketCommonProtocol.fail_connection` are the only +places where :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` may +be canceled. + +:attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task` starts by +waiting for :attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task`. It +catches :exc:`~asyncio.CancelledError` to prevent a cancellation of +:attr:`~legacy.protocol.WebSocketCommonProtocol.transfer_data_task` from propagating +to :attr:`~legacy.protocol.WebSocketCommonProtocol.close_connection_task`. + +.. _backpressure: + +Backpressure +------------ + +.. note:: + + This section discusses backpressure from the perspective of a server but + the concept applies to clients symmetrically. + +With a naive implementation, if a server receives inputs faster than it can +process them, or if it generates outputs faster than it can send them, data +accumulates in buffers, eventually causing the server to run out of memory and +crash. + +The solution to this problem is backpressure. Any part of the server that +receives inputs faster than it can process them and send the outputs +must propagate that information back to the previous part in the chain. + +websockets is designed to make it easy to get backpressure right. + +For incoming data, websockets builds upon :class:`~asyncio.StreamReader` which +propagates backpressure to its own buffer and to the TCP stream. Frames are +parsed from the input stream and added to a bounded queue. If the queue fills +up, parsing halts until the application reads a frame. + +For outgoing data, websockets builds upon :class:`~asyncio.StreamWriter` which +implements flow control. If the output buffers grow too large, it waits until +they're drained. That's why all APIs that write frames are asynchronous. + +Of course, it's still possible for an application to create its own unbounded +buffers and break the backpressure. Be careful with queues. + + +.. _buffers: + +Buffers +------- + +.. note:: + + This section discusses buffers from the perspective of a server but it + applies to clients as well. + +An asynchronous systems works best when its buffers are almost always empty. + +For example, if a client sends data too fast for a server, the queue of +incoming messages will be constantly full. The server will always be 32 +messages (by default) behind the client. This consumes memory and increases +latency for no good reason. The problem is called bufferbloat. + +If buffers are almost always full and that problem cannot be solved by adding +capacity — typically because the system is bottlenecked by the output and +constantly regulated by backpressure — reducing the size of buffers minimizes +negative consequences. + +By default websockets has rather high limits. You can decrease them according +to your application's characteristics. + +Bufferbloat can happen at every level in the stack where there is a buffer. +For each connection, the receiving side contains these buffers: + +- OS buffers: tuning them is an advanced optimization. +- :class:`~asyncio.StreamReader` bytes buffer: the default limit is 64 KiB. + You can set another limit by passing a ``read_limit`` keyword argument to + :func:`~client.connect()` or :func:`~server.serve`. +- Incoming messages :class:`~collections.deque`: its size depends both on + the size and the number of messages it contains. By default the maximum + UTF-8 encoded size is 1 MiB and the maximum number is 32. In the worst case, + after UTF-8 decoding, a single message could take up to 4 MiB of memory and + the overall memory consumption could reach 128 MiB. You should adjust these + limits by setting the ``max_size`` and ``max_queue`` keyword arguments of + :func:`~client.connect()` or :func:`~server.serve` according to your + application's requirements. + +For each connection, the sending side contains these buffers: + +- :class:`~asyncio.StreamWriter` bytes buffer: the default size is 64 KiB. + You can set another limit by passing a ``write_limit`` keyword argument to + :func:`~client.connect()` or :func:`~server.serve`. +- OS buffers: tuning them is an advanced optimization. + +Concurrency +----------- + +Awaiting any combination of :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`, +:meth:`~legacy.protocol.WebSocketCommonProtocol.send`, +:meth:`~legacy.protocol.WebSocketCommonProtocol.close` +:meth:`~legacy.protocol.WebSocketCommonProtocol.ping`, or +:meth:`~legacy.protocol.WebSocketCommonProtocol.pong` concurrently is safe, including +multiple calls to the same method, with one exception and one limitation. + +* **Only one coroutine can receive messages at a time.** This constraint + avoids non-deterministic behavior (and simplifies the implementation). If a + coroutine is awaiting :meth:`~legacy.protocol.WebSocketCommonProtocol.recv`, + awaiting it again in another coroutine raises :exc:`RuntimeError`. + +* **Sending a fragmented message forces serialization.** Indeed, the WebSocket + protocol doesn't support multiplexing messages. If a coroutine is awaiting + :meth:`~legacy.protocol.WebSocketCommonProtocol.send` to send a fragmented message, + awaiting it again in another coroutine waits until the first call completes. + This will be transparent in many cases. It may be a concern if the + fragmented message is generated slowly by an asynchronous iterator. + +Receiving frames is independent from sending frames. This isolates +:meth:`~legacy.protocol.WebSocketCommonProtocol.recv`, which receives frames, from +the other methods, which send frames. + +While the connection is open, each frame is sent with a single write. Combined +with the concurrency model of :mod:`asyncio`, this enforces serialization. The +only other requirement is to prevent interleaving other data frames in the +middle of a fragmented message. + +After the connection is closed, sending a frame raises +:exc:`~websockets.exceptions.ConnectionClosed`, which is safe. diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/topics/index.rst b/tests/wpt/tests/tools/third_party/websockets/docs/topics/index.rst new file mode 100644 index 00000000000..120a3dd3277 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/topics/index.rst @@ -0,0 +1,18 @@ +Topic guides +============ + +Get a deeper understanding of how websockets is built and why. + +.. toctree:: + :titlesonly: + + deployment + logging + authentication + broadcast + compression + timeouts + design + memory + security + performance diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/topics/lifecycle.graffle b/tests/wpt/tests/tools/third_party/websockets/docs/topics/lifecycle.graffle Binary files differnew file mode 100644 index 00000000000..a8ab7ff09f5 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/topics/lifecycle.graffle diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/topics/lifecycle.svg b/tests/wpt/tests/tools/third_party/websockets/docs/topics/lifecycle.svg new file mode 100644 index 00000000000..0a9818d2930 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/topics/lifecycle.svg @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="-14.3464565 112.653543 624.6929 372.69291" width="624.6929pt" height="372.69291pt" xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata> Produced by OmniGraffle 6.6.2 <dc:date>2018-07-29 15:25:34 +0000</dc:date></metadata><defs><font-face font-family="Courier New" font-size="12" panose-1="2 7 6 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="100.097656" slope="0" x-height="443.35938" cap-height="591.79688" ascent="832.51953" descent="-300.29297" font-weight="bold"><font-face-src><font-face-name name="CourierNewPS-BoldMT"/></font-face-src></font-face><font-face font-family="Courier New" font-size="12" panose-1="2 7 3 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="41.015625" slope="0" x-height="422.85156" cap-height="571.28906" ascent="832.51953" descent="-300.29297" font-weight="500"><font-face-src><font-face-name name="CourierNewPSMT"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="StickArrow_Marker" viewBox="-1 -4 8 8" markerWidth="8" markerHeight="8" color="black"><g><path d="M 5.8666667 0 L 0 0 M 0 -2.2 L 5.8666667 0 L 0 2.2" fill="none" stroke="currentColor" stroke-width="1"/></g></marker><font-face font-family="Verdana" font-size="12" panose-1="2 11 6 4 3 5 4 4 2 4" units-per-em="1000" underline-position="-87.890625" underline-thickness="58.59375" slope="0" x-height="545.41016" cap-height="727.0508" ascent="1005.3711" descent="-209.96094" font-weight="500"><font-face-src><font-face-name name="Verdana"/></font-face-src></font-face></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><g><title>Layer 1</title><text transform="translate(19.173228 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="1.5138254" y="10" textLength="72.01172">CONNECTING</tspan></text><text transform="translate(160.90551 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="23.117341" y="10" textLength="28.804688">OPEN</tspan></text><text transform="translate(359.3307 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="12.315583" y="10" textLength="50.408203">CLOSING</tspan></text><text transform="translate(501.063 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="15.916169" y="10" textLength="43.20703">CLOSED</tspan></text><line x1="198.4252" y1="170.07874" x2="198.4252" y2="453.5433" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-dasharray="1,3"/><line x1="396.8504" y1="170.07874" x2="396.8504" y2="453.5433" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-dasharray="1,3"/><line x1="538.58267" y1="170.07874" x2="538.58267" y2="453.5433" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-dasharray="1,3"/><line x1="56.692913" y1="170.07874" x2="56.692913" y2="453.5433" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" stroke-dasharray="1,3"/><path d="M 240.94488 240.94488 L 411.02362 240.94488 C 418.85128 240.94488 425.19685 247.29045 425.19685 255.11811 L 425.19685 255.11811 C 425.19685 262.94577 418.85128 269.29134 411.02362 269.29134 L 240.94488 269.29134 C 233.11722 269.29134 226.77165 262.94577 226.77165 255.11811 L 226.77165 255.11811 C 226.77165 247.29045 233.11722 240.94488 240.94488 240.94488 Z" fill="#dadada"/><path d="M 240.94488 240.94488 L 411.02362 240.94488 C 418.85128 240.94488 425.19685 247.29045 425.19685 255.11811 L 425.19685 255.11811 C 425.19685 262.94577 418.85128 269.29134 411.02362 269.29134 L 240.94488 269.29134 C 233.11722 269.29134 226.77165 262.94577 226.77165 255.11811 L 226.77165 255.11811 C 226.77165 247.29045 233.11722 240.94488 240.94488 240.94488 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(226.77165 248.11811)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="52.40498" y="10" textLength="93.615234">transfer_data</tspan></text><path d="M 240.94488 354.3307 L 552.7559 354.3307 C 560.58356 354.3307 566.92913 360.67628 566.92913 368.50393 L 566.92913 368.50393 C 566.92913 376.3316 560.58356 382.67716 552.7559 382.67716 L 240.94488 382.67716 C 233.11722 382.67716 226.77165 376.3316 226.77165 368.50393 L 226.77165 368.50393 C 226.77165 360.67628 233.11722 354.3307 240.94488 354.3307 Z" fill="#dadada"/><path d="M 240.94488 354.3307 L 552.7559 354.3307 C 560.58356 354.3307 566.92913 360.67628 566.92913 368.50393 L 566.92913 368.50393 C 566.92913 376.3316 560.58356 382.67716 552.7559 382.67716 L 240.94488 382.67716 C 233.11722 382.67716 226.77165 376.3316 226.77165 368.50393 L 226.77165 368.50393 C 226.77165 360.67628 233.11722 354.3307 240.94488 354.3307 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(231.77165 361.50393)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="107.469364" y="10" textLength="115.21875">close_connection</tspan></text><path d="M 99.2126 184.25197 L 155.90551 184.25197 C 163.73317 184.25197 170.07874 190.59754 170.07874 198.4252 L 170.07874 198.4252 C 170.07874 206.25285 163.73317 212.59842 155.90551 212.59842 L 99.2126 212.59842 C 91.38494 212.59842 85.03937 206.25285 85.03937 198.4252 L 85.03937 198.4252 C 85.03937 190.59754 91.38494 184.25197 99.2126 184.25197 Z" fill="#6f6"/><path d="M 99.2126 184.25197 L 155.90551 184.25197 C 163.73317 184.25197 170.07874 190.59754 170.07874 198.4252 L 170.07874 198.4252 C 170.07874 206.25285 163.73317 212.59842 155.90551 212.59842 L 99.2126 212.59842 C 91.38494 212.59842 85.03937 206.25285 85.03937 198.4252 L 85.03937 198.4252 C 85.03937 190.59754 91.38494 184.25197 99.2126 184.25197 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 191.4252)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="12.315583" y="10" textLength="50.408203">connect</tspan></text><path d="M 240.94488 184.25197 L 496.063 184.25197 C 503.89065 184.25197 510.23622 190.59754 510.23622 198.4252 L 510.23622 198.4252 C 510.23622 206.25285 503.89065 212.59842 496.063 212.59842 L 240.94488 212.59842 C 233.11722 212.59842 226.77165 206.25285 226.77165 198.4252 L 226.77165 198.4252 C 226.77165 190.59754 233.11722 184.25197 240.94488 184.25197 Z" fill="#6f6"/><path d="M 240.94488 184.25197 L 496.063 184.25197 C 503.89065 184.25197 510.23622 190.59754 510.23622 198.4252 L 510.23622 198.4252 C 510.23622 206.25285 503.89065 212.59842 496.063 212.59842 L 240.94488 212.59842 C 233.11722 212.59842 226.77165 206.25285 226.77165 198.4252 L 226.77165 198.4252 C 226.77165 190.59754 233.11722 184.25197 240.94488 184.25197 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(231.77165 191.4252)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="17.912947" y="10" textLength="100.816406">recv / send / </tspan><tspan font-family="Courier New" font-size="12" font-weight="500" x="118.72935" y="10" textLength="93.615234">ping / pong /</tspan><tspan font-family="Courier New" font-size="12" font-weight="bold" x="212.34459" y="10" textLength="50.408203"> close </tspan></text><path d="M 170.07874 198.4252 L 183.97874 198.4252 L 198.4252 198.4252 L 198.4252 283.46457 L 198.4252 368.50393 L 212.87165 368.50393 L 215.37165 368.50393" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(75.86614 410.19685)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="27.760296" y="12" textLength="52.083984">opening </tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="19.164593" y="27" textLength="58.02539">handshak</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="77.072796" y="27" textLength="7.1484375">e</tspan></text><text transform="translate(416.02362 410.19685)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="19.182171" y="12" textLength="65.021484">connection</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="16.861858" y="27" textLength="69.66211">termination</tspan></text><text transform="translate(217.59842 410.19685)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="41.03058" y="12" textLength="40.6875">data tr</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="81.507143" y="12" textLength="37.541016">ansfer</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="18.211245" y="27" textLength="116.625">& closing handshak</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="134.71906" y="27" textLength="7.1484375">e</tspan></text><path d="M 425.19685 255.11811 L 439.09685 255.11811 L 453.5433 255.11811 L 453.5433 342.9307" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 240.94488 297.6378 L 411.02362 297.6378 C 418.85128 297.6378 425.19685 303.98336 425.19685 311.81102 L 425.19685 311.81102 C 425.19685 319.63868 418.85128 325.98425 411.02362 325.98425 L 240.94488 325.98425 C 233.11722 325.98425 226.77165 319.63868 226.77165 311.81102 L 226.77165 311.81102 C 226.77165 303.98336 233.11722 297.6378 240.94488 297.6378 Z" fill="#dadada"/><path d="M 240.94488 297.6378 L 411.02362 297.6378 C 418.85128 297.6378 425.19685 303.98336 425.19685 311.81102 L 425.19685 311.81102 C 425.19685 319.63868 418.85128 325.98425 411.02362 325.98425 L 240.94488 325.98425 C 233.11722 325.98425 226.77165 319.63868 226.77165 311.81102 L 226.77165 311.81102 C 226.77165 303.98336 233.11722 297.6378 240.94488 297.6378 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(226.77165 304.81102)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="48.804395" y="10" textLength="100.816406">keepalive_ping</tspan></text><line x1="198.4252" y1="255.11811" x2="214.62165" y2="255.11811" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="198.4252" y1="311.81102" x2="215.37165" y2="311.81102" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/></g></g></svg> diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/topics/logging.rst b/tests/wpt/tests/tools/third_party/websockets/docs/topics/logging.rst new file mode 100644 index 00000000000..e7abd96ce50 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/topics/logging.rst @@ -0,0 +1,245 @@ +Logging +======= + +.. currentmodule:: websockets + +Logs contents +------------- + +When you run a WebSocket client, your code calls coroutines provided by +websockets. + +If an error occurs, websockets tells you by raising an exception. For example, +it raises a :exc:`~exceptions.ConnectionClosed` exception if the other side +closes the connection. + +When you run a WebSocket server, websockets accepts connections, performs the +opening handshake, runs the connection handler coroutine that you provided, +and performs the closing handshake. + +Given this `inversion of control`_, if an error happens in the opening +handshake or if the connection handler crashes, there is no way to raise an +exception that you can handle. + +.. _inversion of control: https://en.wikipedia.org/wiki/Inversion_of_control + +Logs tell you about these errors. + +Besides errors, you may want to record the activity of the server. + +In a request/response protocol such as HTTP, there's an obvious way to record +activity: log one event per request/response. Unfortunately, this solution +doesn't work well for a bidirectional protocol such as WebSocket. + +Instead, when running as a server, websockets logs one event when a +`connection is established`_ and another event when a `connection is +closed`_. + +.. _connection is established: https://www.rfc-editor.org/rfc/rfc6455.html#section-4 +.. _connection is closed: https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.4 + +By default, websockets doesn't log an event for every message. That would be +excessive for many applications exchanging small messages at a fast rate. If +you need this level of detail, you could add logging in your own code. + +Finally, you can enable debug logs to get details about everything websockets +is doing. This can be useful when developing clients as well as servers. + +See :ref:`log levels <log-levels>` below for a list of events logged by +websockets logs at each log level. + +Configure logging +----------------- + +websockets relies on the :mod:`logging` module from the standard library in +order to maximize compatibility and integrate nicely with other libraries:: + + import logging + +websockets logs to the ``"websockets.client"`` and ``"websockets.server"`` +loggers. + +websockets doesn't provide a default logging configuration because +requirements vary a lot depending on the environment. + +Here's a basic configuration for a server in production:: + + logging.basicConfig( + format="%(asctime)s %(message)s", + level=logging.INFO, + ) + +Here's how to enable debug logs for development:: + + logging.basicConfig( + format="%(message)s", + level=logging.DEBUG, + ) + +Furthermore, websockets adds a ``websocket`` attribute to log records, so you +can include additional information about the current connection in logs. + +You could attempt to add information with a formatter:: + + # this doesn't work! + logging.basicConfig( + format="{asctime} {websocket.id} {websocket.remote_address[0]} {message}", + level=logging.INFO, + style="{", + ) + +However, this technique runs into two problems: + +* The formatter applies to all records. It will crash if it receives a record + without a ``websocket`` attribute. For example, this happens when logging + that the server starts because there is no current connection. + +* Even with :meth:`str.format` style, you're restricted to attribute and index + lookups, which isn't enough to implement some fairly simple requirements. + +There's a better way. :func:`~client.connect` and :func:`~server.serve` accept +a ``logger`` argument to override the default :class:`~logging.Logger`. You +can set ``logger`` to a :class:`~logging.LoggerAdapter` that enriches logs. + +For example, if the server is behind a reverse +proxy, :attr:`~legacy.protocol.WebSocketCommonProtocol.remote_address` gives +the IP address of the proxy, which isn't useful. IP addresses of clients are +provided in an HTTP header set by the proxy. + +Here's how to include them in logs, assuming they're in the +``X-Forwarded-For`` header:: + + logging.basicConfig( + format="%(asctime)s %(message)s", + level=logging.INFO, + ) + + class LoggerAdapter(logging.LoggerAdapter): + """Add connection ID and client IP address to websockets logs.""" + def process(self, msg, kwargs): + try: + websocket = kwargs["extra"]["websocket"] + except KeyError: + return msg, kwargs + xff = websocket.request_headers.get("X-Forwarded-For") + return f"{websocket.id} {xff} {msg}", kwargs + + async with websockets.serve( + ..., + # Python < 3.10 requires passing None as the second argument. + logger=LoggerAdapter(logging.getLogger("websockets.server"), None), + ): + ... + +Logging to JSON +--------------- + +Even though :mod:`logging` predates structured logging, it's still possible to +output logs as JSON with a bit of effort. + +First, we need a :class:`~logging.Formatter` that renders JSON: + +.. literalinclude:: ../../example/logging/json_log_formatter.py + +Then, we configure logging to apply this formatter:: + + handler = logging.StreamHandler() + handler.setFormatter(formatter) + + logger = logging.getLogger() + logger.addHandler(handler) + logger.setLevel(logging.INFO) + +Finally, we populate the ``event_data`` custom attribute in log records with +a :class:`~logging.LoggerAdapter`:: + + class LoggerAdapter(logging.LoggerAdapter): + """Add connection ID and client IP address to websockets logs.""" + def process(self, msg, kwargs): + try: + websocket = kwargs["extra"]["websocket"] + except KeyError: + return msg, kwargs + kwargs["extra"]["event_data"] = { + "connection_id": str(websocket.id), + "remote_addr": websocket.request_headers.get("X-Forwarded-For"), + } + return msg, kwargs + + async with websockets.serve( + ..., + # Python < 3.10 requires passing None as the second argument. + logger=LoggerAdapter(logging.getLogger("websockets.server"), None), + ): + ... + +Disable logging +--------------- + +If your application doesn't configure :mod:`logging`, Python outputs messages +of severity ``WARNING`` and higher to :data:`~sys.stderr`. As a consequence, +you will see a message and a stack trace if a connection handler coroutine +crashes or if you hit a bug in websockets. + +If you want to disable this behavior for websockets, you can add +a :class:`~logging.NullHandler`:: + + logging.getLogger("websockets").addHandler(logging.NullHandler()) + +Additionally, if your application configures :mod:`logging`, you must disable +propagation to the root logger, or else its handlers could output logs:: + + logging.getLogger("websockets").propagate = False + +Alternatively, you could set the log level to ``CRITICAL`` for the +``"websockets"`` logger, as the highest level currently used is ``ERROR``:: + + logging.getLogger("websockets").setLevel(logging.CRITICAL) + +Or you could configure a filter to drop all messages:: + + logging.getLogger("websockets").addFilter(lambda record: None) + +.. _log-levels: + +Log levels +---------- + +Here's what websockets logs at each level. + +``ERROR`` +......... + +* Exceptions raised by connection handler coroutines in servers +* Exceptions resulting from bugs in websockets + +``WARNING`` +........... + +* Failures in :func:`~websockets.broadcast` + +``INFO`` +........ + +* Server starting and stopping +* Server establishing and closing connections +* Client reconnecting automatically + +``DEBUG`` +......... + +* Changes to the state of connections +* Handshake requests and responses +* All frames sent and received +* Steps to close a connection +* Keepalive pings and pongs +* Errors handled transparently + +Debug messages have cute prefixes that make logs easier to scan: + +* ``>`` - send something +* ``<`` - receive something +* ``=`` - set connection state +* ``x`` - shut down connection +* ``%`` - manage pings and pongs +* ``!`` - handle errors and timeouts diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/topics/memory.rst b/tests/wpt/tests/tools/third_party/websockets/docs/topics/memory.rst new file mode 100644 index 00000000000..e44247a77c5 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/topics/memory.rst @@ -0,0 +1,48 @@ +Memory usage +============ + +.. currentmodule:: websockets + +In most cases, memory usage of a WebSocket server is proportional to the +number of open connections. When a server handles thousands of connections, +memory usage can become a bottleneck. + +Memory usage of a single connection is the sum of: + +1. the baseline amount of memory websockets requires for each connection, +2. the amount of data held in buffers before the application processes it, +3. any additional memory allocated by the application itself. + +Baseline +-------- + +Compression settings are the main factor affecting the baseline amount of +memory used by each connection. + +With websockets' defaults, on the server side, a single connections uses +70 KiB of memory. + +Refer to the :doc:`topic guide on compression <../topics/compression>` to +learn more about tuning compression settings. + +Buffers +------- + +Under normal circumstances, buffers are almost always empty. + +Under high load, if a server receives more messages than it can process, +bufferbloat can result in excessive memory usage. + +By default websockets has generous limits. It is strongly recommended to adapt +them to your application. When you call :func:`~server.serve`: + +- Set ``max_size`` (default: 1 MiB, UTF-8 encoded) to the maximum size of + messages your application generates. +- Set ``max_queue`` (default: 32) to the maximum number of messages your + application expects to receive faster than it can process them. The queue + provides burst tolerance without slowing down the TCP connection. + +Furthermore, you can lower ``read_limit`` and ``write_limit`` (default: +64 KiB) to reduce the size of buffers for incoming and outgoing data. + +The design document provides :ref:`more details about buffers <buffers>`. diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/topics/performance.rst b/tests/wpt/tests/tools/third_party/websockets/docs/topics/performance.rst new file mode 100644 index 00000000000..45e23b2390a --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/topics/performance.rst @@ -0,0 +1,20 @@ +Performance +=========== + +Here are tips to optimize performance. + +uvloop +------ + +You can make a websockets application faster by running it with uvloop_. + +(This advice isn't specific to websockets. It applies to any :mod:`asyncio` +application.) + +.. _uvloop: https://github.com/MagicStack/uvloop + +broadcast +--------- + +:func:`~websockets.broadcast` is the most efficient way to send a message to +many clients. diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/topics/protocol.graffle b/tests/wpt/tests/tools/third_party/websockets/docs/topics/protocol.graffle Binary files differnew file mode 100644 index 00000000000..df76f49607e --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/topics/protocol.graffle diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/topics/protocol.svg b/tests/wpt/tests/tools/third_party/websockets/docs/topics/protocol.svg new file mode 100644 index 00000000000..51bfd982be7 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/topics/protocol.svg @@ -0,0 +1,3 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xl="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 624.34646 822.34646" width="624.34646pt" height="822.34646pt" xmlns:dc="http://purl.org/dc/elements/1.1/"><metadata> Produced by OmniGraffle 6.6.2 <dc:date>2019-07-07 08:38:24 +0000</dc:date></metadata><defs><font-face font-family="Verdana" font-size="12" panose-1="2 11 6 4 3 5 4 4 2 4" units-per-em="1000" underline-position="-87.890625" underline-thickness="58.59375" slope="0" x-height="545.41016" cap-height="727.0508" ascent="1005.3711" descent="-209.96094" font-weight="500"><font-face-src><font-face-name name="Verdana"/></font-face-src></font-face><font-face font-family="Courier New" font-size="12" panose-1="2 7 3 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="41.015625" slope="0" x-height="422.85156" cap-height="571.28906" ascent="832.51953" descent="-300.29297" font-weight="500"><font-face-src><font-face-name name="CourierNewPSMT"/></font-face-src></font-face><font-face font-family="Courier New" font-size="12" panose-1="2 7 6 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="100.097656" slope="0" x-height="443.35938" cap-height="591.79688" ascent="832.51953" descent="-300.29297" font-weight="bold"><font-face-src><font-face-name name="CourierNewPS-BoldMT"/></font-face-src></font-face><font-face font-family="Courier New" font-size="10" panose-1="2 7 3 9 2 2 5 2 4 4" units-per-em="1000" underline-position="-232.91016" underline-thickness="41.015625" slope="0" x-height="422.85156" cap-height="571.28906" ascent="832.51953" descent="-300.29297" font-weight="500"><font-face-src><font-face-name name="CourierNewPSMT"/></font-face-src></font-face><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="StickArrow_Marker" viewBox="-1 -4 8 8" markerWidth="8" markerHeight="8" color="black"><g><path d="M 5.8666667 0 L 0 0 M 0 -2.2 L 5.8666667 0 L 0 2.2" fill="none" stroke="currentColor" stroke-width="1"/></g></marker><radialGradient cx="0" cy="0" r="1" id="Gradient" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="white"/><stop offset="1" stop-color="#a5a5a5"/></radialGradient><radialGradient id="Obj_Gradient" xl:href="#Gradient" gradientTransform="translate(311.81102 708.6614) scale(145.75703)"/><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="StickArrow_Marker_2" viewBox="-1 -6 14 12" markerWidth="14" markerHeight="12" color="black"><g><path d="M 12 0 L 0 0 M 0 -4.5 L 12 0 L 0 4.5" fill="none" stroke="currentColor" stroke-width="1"/></g></marker><marker orient="auto" overflow="visible" markerUnits="strokeWidth" id="StickArrow_Marker_3" viewBox="-1 -4 8 8" markerWidth="8" markerHeight="8" color="black"><g><path d="M 5.9253333 0 L 0 0 M 0 -2.222 L 5.9253333 0 L 0 2.222" fill="none" stroke="currentColor" stroke-width="1"/></g></marker></defs><g stroke="none" stroke-opacity="1" stroke-dasharray="none" fill="none" fill-opacity="1"><title>Canvas 1</title><rect fill="white" width="1314" height="1698"/><g><title>Layer 1</title><rect x="28.346457" y="765.35433" width="566.92913" height="28.346457" fill="#6cf"/><rect x="28.346457" y="765.35433" width="566.92913" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(33.346457 772.02755)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="228.50753" y="12" textLength="99.91406">remote endpoint</tspan></text><rect x="28.346457" y="85.03937" width="566.92913" height="566.92913" fill="white"/><rect x="28.346457" y="85.03937" width="566.92913" height="566.92913" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(33.346457 90.03937)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="243.79171" y="12" textLength="51.333984">websock</tspan><tspan font-family="Verdana" font-size="12" font-weight="500" x="295.00851" y="12" textLength="18.128906">ets</tspan><tspan font-family="Courier New" font-size="12" font-weight="500" x="195.65109" y="25" textLength="165.62695">WebSocketCommonProtocol</tspan></text><rect x="28.346457" y="28.346457" width="566.92913" height="28.346457" fill="#6f6"/><rect x="28.346457" y="28.346457" width="566.92913" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(33.346457 35.019685)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="230.0046" y="12" textLength="96.91992">application logic</tspan></text><path d="M 102.047243 586.77165 L 238.11023 586.77165 C 247.49858 586.77165 255.11811 596.93102 255.11811 609.4488 C 255.11811 621.9666 247.49858 632.12598 238.11023 632.12598 L 102.047243 632.12598 C 92.658897 632.12598 85.03937 621.9666 85.03937 609.4488 C 85.03937 596.93102 92.658897 586.77165 102.047243 586.77165" fill="#fc6"/><path d="M 102.047243 586.77165 L 238.11023 586.77165 C 247.49858 586.77165 255.11811 596.93102 255.11811 609.4488 C 255.11811 621.9666 247.49858 632.12598 238.11023 632.12598 L 102.047243 632.12598 C 92.658897 632.12598 85.03937 621.9666 85.03937 609.4488 C 85.03937 596.93102 92.658897 586.77165 102.047243 586.77165 M 238.11023 586.77165 C 228.72189 586.77165 221.10236 596.93102 221.10236 609.4488 C 221.10236 621.9666 228.72189 632.12598 238.11023 632.12598" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(125.33071 596.9488)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="14.896484" y="10" textLength="43.20703">reader</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x=".49414062" y="22" textLength="72.01172">StreamReader</tspan></text><path d="M 385.5118 586.77165 L 521.5748 586.77165 C 530.96315 586.77165 538.58267 596.93102 538.58267 609.4488 C 538.58267 621.9666 530.96315 632.12598 521.5748 632.12598 L 385.5118 632.12598 C 376.12346 632.12598 368.50393 621.9666 368.50393 609.4488 C 368.50393 596.93102 376.12346 586.77165 385.5118 586.77165" fill="#fc6"/><path d="M 385.5118 586.77165 L 521.5748 586.77165 C 530.96315 586.77165 538.58267 596.93102 538.58267 609.4488 C 538.58267 621.9666 530.96315 632.12598 521.5748 632.12598 L 385.5118 632.12598 C 376.12346 632.12598 368.50393 621.9666 368.50393 609.4488 C 368.50393 596.93102 376.12346 586.77165 385.5118 586.77165 M 521.5748 586.77165 C 512.18645 586.77165 504.56693 596.93102 504.56693 609.4488 C 504.56693 621.9666 512.18645 632.12598 521.5748 632.12598" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(408.79527 596.9488)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="14.896484" y="10" textLength="43.20703">writer</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x=".49414062" y="22" textLength="72.01172">StreamWriter</tspan></text><path d="M 481.88976 419.52756 L 481.88976 374.17323 C 481.88976 371.04378 469.19055 368.50393 453.5433 368.50393 C 437.89606 368.50393 425.19685 371.04378 425.19685 374.17323 L 425.19685 419.52756 C 425.19685 422.657 437.89606 425.19685 453.5433 425.19685 C 469.19055 425.19685 481.88976 422.657 481.88976 419.52756" fill="#fecc66"/><path d="M 481.88976 419.52756 L 481.88976 374.17323 C 481.88976 371.04378 469.19055 368.50393 453.5433 368.50393 C 437.89606 368.50393 425.19685 371.04378 425.19685 374.17323 L 425.19685 419.52756 C 425.19685 422.657 437.89606 425.19685 453.5433 425.19685 C 469.19055 425.19685 481.88976 422.657 481.88976 419.52756 M 481.88976 374.17323 C 481.88976 377.30267 469.19055 379.84252 453.5433 379.84252 C 437.89606 379.84252 425.19685 377.30267 425.19685 374.17323" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><text transform="translate(429.19685 387.18504)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="500" x="6.343527" y="10" textLength="36.00586">pings</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="12.3445034" y="22" textLength="24.003906">dict</tspan></text><path d="M 85.039413 283.46457 L 255.11806 283.46457 C 270.7734 283.46457 283.46457 296.15573 283.46457 311.81107 L 283.46457 481.88972 C 283.46457 497.54506 270.7734 510.23622 255.11806 510.23622 L 85.039413 510.23622 C 69.384074 510.23622 56.692913 497.54506 56.692913 481.88972 L 56.692913 311.81107 C 56.692913 296.15573 69.384074 283.46457 85.039413 283.46457 Z" fill="#dadada"/><path d="M 85.039413 283.46457 L 255.11806 283.46457 C 270.7734 283.46457 283.46457 296.15573 283.46457 311.81107 L 283.46457 481.88972 C 283.46457 497.54506 270.7734 510.23622 255.11806 510.23622 L 85.039413 510.23622 C 69.384074 510.23622 56.692913 497.54506 56.692913 481.88972 L 56.692913 311.81107 C 56.692913 296.15573 69.384074 283.46457 85.039413 283.46457 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(61.692913 288.46457)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="43.57528" y="10" textLength="129.62109">transfer_data_task</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="96.383873" y="22" textLength="24.003906">Task</tspan></text><path d="M 297.6378 765.35433 L 297.6378 609.4488 L 255.11811 609.4488 L 269.01811 609.4488 L 266.51811 609.4488" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 368.50393 609.4488 L 354.60393 609.4488 L 325.98425 609.4488 L 325.98425 753.95433" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 207.03401 712.3154 C 161.22047 708.6614 179.48976 677.90097 252.5726 683.1496 C 259.35307 672.91835 344.33858 674.579 343.783 683.1496 C 397.0715 672.1877 465.17102 694.04553 419.49354 705.00744 C 474.30425 710.32206 418.80189 738.9565 373.8189 734.17322 C 370.2189 742.14584 289.80283 744.9358 282.74457 734.17322 C 237.20882 745.66715 142.25953 727.9946 207.03401 712.3154 Z" fill="url(#Obj_Gradient)"/><path d="M 207.03401 712.3154 C 161.22047 708.6614 179.48976 677.90097 252.5726 683.1496 C 259.35307 672.91835 344.33858 674.579 343.783 683.1496 C 397.0715 672.1877 465.17102 694.04553 419.49354 705.00744 C 474.30425 710.32206 418.80189 738.9565 373.8189 734.17322 C 370.2189 742.14584 289.80283 744.9358 282.74457 734.17322 C 237.20882 745.66715 142.25953 727.9946 207.03401 712.3154 Z" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(217.59842 701.1614)" fill="black"><tspan font-family="Verdana" font-size="12" font-weight="500" x="69.81416" y="12" textLength="48.796875">network</tspan></text><rect x="85.03937" y="453.5433" width="170.07874" height="28.346457" fill="#ff6"/><rect x="85.03937" y="453.5433" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 460.71653)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="44.03351" y="10" textLength="72.01172">read_frame</tspan></text><rect x="85.03937" y="396.8504" width="170.07874" height="28.346457" fill="#ff6"/><rect x="85.03937" y="396.8504" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 404.02362)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="26.03058" y="10" textLength="108.01758">read_data_frame</tspan></text><rect x="85.03937" y="340.15748" width="170.07874" height="28.346457" fill="#ff6"/><rect x="85.03937" y="340.15748" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 347.3307)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="36.832338" y="10" textLength="86.41406">read_message</tspan></text><text transform="translate(178.07874 490.563)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="30.004883">bytes</tspan></text><text transform="translate(178.07874 433.87008)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="36.00586">frames</tspan></text><text transform="translate(178.07874 371.67716)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="24.003906">data</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="19" textLength="36.00586">frames</tspan></text><rect x="368.50393" y="510.23622" width="170.07874" height="28.346457" fill="#ff6"/><rect x="368.50393" y="510.23622" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(373.50393 517.40945)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="40.432924" y="10" textLength="79.21289">write_frame</tspan></text><path d="M 85.03937 609.4488 L 71.13937 609.4488 L 56.692913 609.4488 L 56.692913 595.2756 L 56.692913 566.92913 L 113.385826 566.92913 L 170.07874 566.92913 L 170.07874 495.78976 L 170.07874 494.03976" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 453.5433 539.33267 L 453.5433 552.48267 L 453.5433 566.92913 L 510.23622 566.92913 L 569.76378 566.92913 L 569.76378 595.2756 L 569.76378 609.4488 L 552.48267 609.4488 L 549.98267 609.4488" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="170.07874" y1="453.5433" x2="170.07874" y2="437.34685" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="170.07874" y1="396.8504" x2="170.07874" y2="380.65393" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 102.047243 204.09449 L 238.11023 204.09449 C 247.49858 204.09449 255.11811 214.25386 255.11811 226.77165 C 255.11811 239.28945 247.49858 249.44882 238.11023 249.44882 L 102.047243 249.44882 C 92.658897 249.44882 85.03937 239.28945 85.03937 226.77165 C 85.03937 214.25386 92.658897 204.09449 102.047243 204.09449" fill="#fc6"/><path d="M 102.047243 204.09449 L 238.11023 204.09449 C 247.49858 204.09449 255.11811 214.25386 255.11811 226.77165 C 255.11811 239.28945 247.49858 249.44882 238.11023 249.44882 L 102.047243 249.44882 C 92.658897 249.44882 85.03937 239.28945 85.03937 226.77165 C 85.03937 214.25386 92.658897 204.09449 102.047243 204.09449 M 238.11023 204.09449 C 228.72189 204.09449 221.10236 214.25386 221.10236 226.77165 C 221.10236 239.28945 228.72189 249.44882 238.11023 249.44882" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(132.33071 214.27165)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x=".1953125" y="10" textLength="57.609375">messages</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="13.997559" y="22" textLength="30.004883">deque</tspan></text><path d="M 255.11811 354.3307 L 269.01811 354.3307 L 297.6378 354.3307 L 297.6378 328.8189 L 297.6378 226.77165 L 269.01811 226.77165 L 266.51811 226.77165" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><rect x="85.03937" y="141.73228" width="170.07874" height="28.346457" fill="#cf6"/><rect x="85.03937" y="141.73228" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(90.03937 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="65.637026" y="10" textLength="28.804688">recv</tspan></text><path d="M 85.03937 226.77165 L 71.13937 226.77165 L 42.519685 226.77165 L 42.519685 209.76378 L 42.519685 155.90551 L 71.13937 155.90551 L 73.63937 155.90551" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="170.07874" y1="141.73228" x2="170.07874" y2="68.092913" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="453.5433" y1="56.692913" x2="453.5433" y2="130.33228" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="467.71653" y1="56.692913" x2="467.71653" y2="187.8752" marker-end="url(#StickArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><line x1="481.88976" y1="56.692913" x2="481.88976" y2="244.56811" marker-end="url(#StickArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><line x1="496.063" y1="56.692913" x2="496.063" y2="300.32302" marker-end="url(#StickArrow_Marker_3)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><rect x="368.50393" y="141.73228" width="170.07874" height="28.346457" fill="#cf6"/><rect x="368.50393" y="141.73228" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(373.50393 148.90551)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="65.637026" y="10" textLength="28.804688">send</tspan></text><rect x="368.50393" y="198.4252" width="170.07874" height="28.346457" fill="#cf6"/><rect x="368.50393" y="198.4252" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><text transform="translate(373.50393 205.59842)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="500" x="65.637026" y="10" textLength="28.804688">ping</tspan></text><rect x="368.50393" y="255.11811" width="170.07874" height="28.346457" fill="#cf6"/><rect x="368.50393" y="255.11811" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><text transform="translate(373.50393 262.29134)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="500" x="65.637026" y="10" textLength="28.804688">pong</tspan></text><rect x="368.50393" y="311.81102" width="170.07874" height="28.346457" fill="#cf6"/><rect x="368.50393" y="311.81102" width="170.07874" height="28.346457" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(373.50393 318.98425)" fill="black"><tspan font-family="Courier New" font-size="12" font-weight="bold" x="62.03644" y="10" textLength="36.00586">close</tspan></text><path d="M 538.58267 155.90551 L 552.48267 155.90551 L 566.92913 155.90551 L 566.92913 481.88976 L 453.5433 481.88976 L 453.5433 496.33622 L 453.5433 498.08622" marker-end="url(#StickArrow_Marker)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><line x1="538.58267" y1="212.59842" x2="566.92913" y2="212.59842" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><line x1="538.58267" y1="269.29134" x2="566.92913" y2="269.29134" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><line x1="538.58267" y1="325.98425" x2="566.92913" y2="325.98425" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><path d="M 255.86811 411.02362 L 262.61811 411.02362 L 340.15748 411.02362 L 340.15748 481.88976 L 453.5433 481.88976" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"/><text transform="translate(291.94527 399.02362)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="42.006836">control</tspan><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="21" textLength="36.00586">frames</tspan></text><line x1="340.15748" y1="411.02362" x2="414.64685" y2="411.02362" marker-end="url(#StickArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><path d="M 368.50393 212.59842 L 361.75393 212.59842 L 340.15748 212.59842 L 340.15748 340.15748 L 340.15748 382.67716" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/><text transform="translate(461.5433 547.2559)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="30.004883">bytes</tspan></text><text transform="translate(461.5433 490.563)" fill="black"><tspan font-family="Courier New" font-size="10" font-weight="500" x="0" y="8" textLength="36.00586">frames</tspan></text><line x1="340.15748" y1="382.67716" x2="414.64685" y2="382.67716" marker-end="url(#StickArrow_Marker_2)" stroke="black" stroke-linecap="round" stroke-linejoin="round" stroke-width=".75"/></g></g></svg> diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/topics/security.rst b/tests/wpt/tests/tools/third_party/websockets/docs/topics/security.rst new file mode 100644 index 00000000000..d3dec21bd10 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/topics/security.rst @@ -0,0 +1,41 @@ +Security +======== + +Encryption +---------- + +For production use, a server should require encrypted connections. + +See this example of :ref:`encrypting connections with TLS +<secure-server-example>`. + +Memory usage +------------ + +.. warning:: + + An attacker who can open an arbitrary number of connections will be able + to perform a denial of service by memory exhaustion. If you're concerned + by denial of service attacks, you must reject suspicious connections + before they reach websockets, typically in a reverse proxy. + +With the default settings, opening a connection uses 70 KiB of memory. + +Sending some highly compressed messages could use up to 128 MiB of memory with +an amplification factor of 1000 between network traffic and memory usage. + +Configuring a server to :doc:`optimize memory usage <memory>` will improve +security in addition to improving performance. + +Other limits +------------ + +websockets implements additional limits on the amount of data it accepts in +order to minimize exposure to security vulnerabilities. + +In the opening handshake, websockets limits the number of HTTP headers to 256 +and the size of an individual header to 4096 bytes. These limits are 10 to 20 +times larger than what's expected in standard use cases. They're hard-coded. + +If you need to change these limits, you can monkey-patch the constants in +``websockets.http11``. diff --git a/tests/wpt/tests/tools/third_party/websockets/docs/topics/timeouts.rst b/tests/wpt/tests/tools/third_party/websockets/docs/topics/timeouts.rst new file mode 100644 index 00000000000..633fc1ab431 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/docs/topics/timeouts.rst @@ -0,0 +1,116 @@ +Timeouts +======== + +.. currentmodule:: websockets + +Long-lived connections +---------------------- + +Since the WebSocket protocol is intended for real-time communications over +long-lived connections, it is desirable to ensure that connections don't +break, and if they do, to report the problem quickly. + +Connections can drop as a consequence of temporary network connectivity issues, +which are very common, even within data centers. + +Furthermore, WebSocket builds on top of HTTP/1.1 where connections are +short-lived, even with ``Connection: keep-alive``. Typically, HTTP/1.1 +infrastructure closes idle connections after 30 to 120 seconds. + +As a consequence, proxies may terminate WebSocket connections prematurely when +no message was exchanged in 30 seconds. + +.. _keepalive: + +Keepalive in websockets +----------------------- + +To avoid these problems, websockets runs a keepalive and heartbeat mechanism +based on WebSocket Ping_ and Pong_ frames, which are designed for this purpose. + +.. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2 +.. _Pong: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.3 + +It loops through these steps: + +1. Wait 20 seconds. +2. Send a Ping frame. +3. Receive a corresponding Pong frame within 20 seconds. + +If the Pong frame isn't received, websockets considers the connection broken and +closes it. + +This mechanism serves two purposes: + +1. It creates a trickle of traffic so that the TCP connection isn't idle and + network infrastructure along the path keeps it open ("keepalive"). +2. It detects if the connection drops or becomes so slow that it's unusable in + practice ("heartbeat"). In that case, it terminates the connection and your + application gets a :exc:`~exceptions.ConnectionClosed` exception. + +Timings are configurable with the ``ping_interval`` and ``ping_timeout`` +arguments of :func:`~client.connect` and :func:`~server.serve`. Shorter values +will detect connection drops faster but they will increase network traffic and +they will be more sensitive to latency. + +Setting ``ping_interval`` to :obj:`None` disables the whole keepalive and +heartbeat mechanism. + +Setting ``ping_timeout`` to :obj:`None` disables only timeouts. This enables +keepalive, to keep idle connections open, and disables heartbeat, to support large +latency spikes. + +.. admonition:: Why doesn't websockets rely on TCP keepalive? + :class: hint + + TCP keepalive is disabled by default on most operating systems. When + enabled, the default interval is two hours or more, which is far too much. + +Keepalive in browsers +--------------------- + +Browsers don't enable a keepalive mechanism like websockets by default. As a +consequence, they can fail to notice that a WebSocket connection is broken for +an extended period of time, until the TCP connection times out. + +In this scenario, the ``WebSocket`` object in the browser doesn't fire a +``close`` event. If you have a reconnection mechanism, it doesn't kick in +because it believes that the connection is still working. + +If your browser-based app mysteriously and randomly fails to receive events, +this is a likely cause. You need a keepalive mechanism in the browser to avoid +this scenario. + +Unfortunately, the WebSocket API in browsers doesn't expose the native Ping and +Pong functionality in the WebSocket protocol. You have to roll your own in the +application layer. + +Latency issues +-------------- + +Latency between a client and a server may increase for two reasons: + +* Network connectivity is poor. When network packets are lost, TCP attempts to + retransmit them, which manifests as latency. Excessive packet loss makes + the connection unusable in practice. At some point, timing out is a + reasonable choice. + +* Traffic is high. For example, if a client sends messages on the connection + faster than a server can process them, this manifests as latency as well, + because data is waiting in flight, mostly in OS buffers. + + If the server is more than 20 seconds behind, it doesn't see the Pong before + the default timeout elapses. As a consequence, it closes the connection. + This is a reasonable choice to prevent overload. + + If traffic spikes cause unwanted timeouts and you're confident that the server + will catch up eventually, you can increase ``ping_timeout`` or you can set it + to :obj:`None` to disable heartbeat entirely. + + The same reasoning applies to situations where the server sends more traffic + than the client can accept. + +The latency measured during the last exchange of Ping and Pong frames is +available in the :attr:`~legacy.protocol.WebSocketCommonProtocol.latency` +attribute. Alternatively, you can measure the latency at any time with the +:attr:`~legacy.protocol.WebSocketCommonProtocol.ping` method. diff --git a/tests/wpt/tests/tools/third_party/websockets/example/deployment/fly/Procfile b/tests/wpt/tests/tools/third_party/websockets/example/deployment/fly/Procfile new file mode 100644 index 00000000000..2e35818f675 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/deployment/fly/Procfile @@ -0,0 +1 @@ +web: python app.py diff --git a/tests/wpt/tests/tools/third_party/websockets/example/deployment/fly/app.py b/tests/wpt/tests/tools/third_party/websockets/example/deployment/fly/app.py new file mode 100644 index 00000000000..4ca34d23bbe --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/deployment/fly/app.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python + +import asyncio +import http +import signal + +import websockets + + +async def echo(websocket): + async for message in websocket: + await websocket.send(message) + + +async def health_check(path, request_headers): + if path == "/healthz": + return http.HTTPStatus.OK, [], b"OK\n" + + +async def main(): + # Set the stop condition when receiving SIGTERM. + loop = asyncio.get_running_loop() + stop = loop.create_future() + loop.add_signal_handler(signal.SIGTERM, stop.set_result, None) + + async with websockets.serve( + echo, + host="", + port=8080, + process_request=health_check, + ): + await stop + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/deployment/fly/fly.toml b/tests/wpt/tests/tools/third_party/websockets/example/deployment/fly/fly.toml new file mode 100644 index 00000000000..5290072ed28 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/deployment/fly/fly.toml @@ -0,0 +1,16 @@ +app = "websockets-echo" +kill_signal = "SIGTERM" + +[build] + builder = "paketobuildpacks/builder:base" + +[[services]] + internal_port = 8080 + protocol = "tcp" + + [[services.http_checks]] + path = "/healthz" + + [[services.ports]] + handlers = ["tls", "http"] + port = 443 diff --git a/tests/wpt/tests/tools/third_party/websockets/example/deployment/fly/requirements.txt b/tests/wpt/tests/tools/third_party/websockets/example/deployment/fly/requirements.txt new file mode 100644 index 00000000000..14774b465e9 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/deployment/fly/requirements.txt @@ -0,0 +1 @@ +websockets diff --git a/tests/wpt/tests/tools/third_party/websockets/example/deployment/haproxy/app.py b/tests/wpt/tests/tools/third_party/websockets/example/deployment/haproxy/app.py new file mode 100644 index 00000000000..360479b8eb6 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/deployment/haproxy/app.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +import asyncio +import os +import signal + +import websockets + + +async def echo(websocket): + async for message in websocket: + await websocket.send(message) + + +async def main(): + # Set the stop condition when receiving SIGTERM. + loop = asyncio.get_running_loop() + stop = loop.create_future() + loop.add_signal_handler(signal.SIGTERM, stop.set_result, None) + + async with websockets.serve( + echo, + host="localhost", + port=8000 + int(os.environ["SUPERVISOR_PROCESS_NAME"][-2:]), + ): + await stop + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/deployment/haproxy/haproxy.cfg b/tests/wpt/tests/tools/third_party/websockets/example/deployment/haproxy/haproxy.cfg new file mode 100644 index 00000000000..e63727d1c0e --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/deployment/haproxy/haproxy.cfg @@ -0,0 +1,17 @@ +defaults + mode http + timeout connect 10s + timeout client 30s + timeout server 30s + +frontend websocket + bind localhost:8080 + default_backend websocket + +backend websocket + balance leastconn + server websockets-test_00 localhost:8000 + server websockets-test_01 localhost:8001 + server websockets-test_02 localhost:8002 + server websockets-test_03 localhost:8003 + diff --git a/tests/wpt/tests/tools/third_party/websockets/example/deployment/haproxy/supervisord.conf b/tests/wpt/tests/tools/third_party/websockets/example/deployment/haproxy/supervisord.conf new file mode 100644 index 00000000000..76a664d91b1 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/deployment/haproxy/supervisord.conf @@ -0,0 +1,7 @@ +[supervisord] + +[program:websockets-test] +command = python app.py +process_name = %(program_name)s_%(process_num)02d +numprocs = 4 +autorestart = true diff --git a/tests/wpt/tests/tools/third_party/websockets/example/deployment/heroku/Procfile b/tests/wpt/tests/tools/third_party/websockets/example/deployment/heroku/Procfile new file mode 100644 index 00000000000..2e35818f675 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/deployment/heroku/Procfile @@ -0,0 +1 @@ +web: python app.py diff --git a/tests/wpt/tests/tools/third_party/websockets/example/deployment/heroku/app.py b/tests/wpt/tests/tools/third_party/websockets/example/deployment/heroku/app.py new file mode 100644 index 00000000000..d4ba3edb511 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/deployment/heroku/app.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +import asyncio +import signal +import os + +import websockets + + +async def echo(websocket): + async for message in websocket: + await websocket.send(message) + + +async def main(): + # Set the stop condition when receiving SIGTERM. + loop = asyncio.get_running_loop() + stop = loop.create_future() + loop.add_signal_handler(signal.SIGTERM, stop.set_result, None) + + async with websockets.serve( + echo, + host="", + port=int(os.environ["PORT"]), + ): + await stop + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/deployment/heroku/requirements.txt b/tests/wpt/tests/tools/third_party/websockets/example/deployment/heroku/requirements.txt new file mode 100644 index 00000000000..14774b465e9 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/deployment/heroku/requirements.txt @@ -0,0 +1 @@ +websockets diff --git a/tests/wpt/tests/tools/third_party/websockets/example/deployment/kubernetes/Dockerfile b/tests/wpt/tests/tools/third_party/websockets/example/deployment/kubernetes/Dockerfile new file mode 100644 index 00000000000..83ed8722c07 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/deployment/kubernetes/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.9-alpine + +RUN pip install websockets + +COPY app.py . + +CMD ["python", "app.py"] diff --git a/tests/wpt/tests/tools/third_party/websockets/example/deployment/kubernetes/app.py b/tests/wpt/tests/tools/third_party/websockets/example/deployment/kubernetes/app.py new file mode 100755 index 00000000000..a8bcef68813 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/deployment/kubernetes/app.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +import asyncio +import http +import signal +import sys +import time + +import websockets + + +async def slow_echo(websocket): + async for message in websocket: + # Block the event loop! This allows saturating a single asyncio + # process without opening an impractical number of connections. + time.sleep(0.1) # 100ms + await websocket.send(message) + + +async def health_check(path, request_headers): + if path == "/healthz": + return http.HTTPStatus.OK, [], b"OK\n" + if path == "/inemuri": + loop = asyncio.get_running_loop() + loop.call_later(1, time.sleep, 10) + return http.HTTPStatus.OK, [], b"Sleeping for 10s\n" + if path == "/seppuku": + loop = asyncio.get_running_loop() + loop.call_later(1, sys.exit, 69) + return http.HTTPStatus.OK, [], b"Terminating\n" + + +async def main(): + # Set the stop condition when receiving SIGTERM. + loop = asyncio.get_running_loop() + stop = loop.create_future() + loop.add_signal_handler(signal.SIGTERM, stop.set_result, None) + + async with websockets.serve( + slow_echo, + host="", + port=80, + process_request=health_check, + ): + await stop + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/deployment/kubernetes/benchmark.py b/tests/wpt/tests/tools/third_party/websockets/example/deployment/kubernetes/benchmark.py new file mode 100755 index 00000000000..22ee4c5bd7b --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/deployment/kubernetes/benchmark.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python + +import asyncio +import sys +import websockets + + +URI = "ws://localhost:32080" + + +async def run(client_id, messages): + async with websockets.connect(URI) as websocket: + for message_id in range(messages): + await websocket.send(f"{client_id}:{message_id}") + await websocket.recv() + + +async def benchmark(clients, messages): + await asyncio.wait([ + asyncio.create_task(run(client_id, messages)) + for client_id in range(clients) + ]) + + +if __name__ == "__main__": + clients, messages = int(sys.argv[1]), int(sys.argv[2]) + asyncio.run(benchmark(clients, messages)) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/deployment/kubernetes/deployment.yaml b/tests/wpt/tests/tools/third_party/websockets/example/deployment/kubernetes/deployment.yaml new file mode 100644 index 00000000000..ba58dd62bf4 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/deployment/kubernetes/deployment.yaml @@ -0,0 +1,35 @@ +apiVersion: v1 +kind: Service +metadata: + name: websockets-test +spec: + type: NodePort + ports: + - port: 80 + nodePort: 32080 + selector: + app: websockets-test +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: websockets-test +spec: + selector: + matchLabels: + app: websockets-test + template: + metadata: + labels: + app: websockets-test + spec: + containers: + - name: websockets-test + image: websockets-test:1.0 + livenessProbe: + httpGet: + path: /healthz + port: 80 + periodSeconds: 1 + ports: + - containerPort: 80 diff --git a/tests/wpt/tests/tools/third_party/websockets/example/deployment/nginx/app.py b/tests/wpt/tests/tools/third_party/websockets/example/deployment/nginx/app.py new file mode 100644 index 00000000000..24e60897562 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/deployment/nginx/app.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python + +import asyncio +import os +import signal + +import websockets + + +async def echo(websocket): + async for message in websocket: + await websocket.send(message) + + +async def main(): + # Set the stop condition when receiving SIGTERM. + loop = asyncio.get_running_loop() + stop = loop.create_future() + loop.add_signal_handler(signal.SIGTERM, stop.set_result, None) + + async with websockets.unix_serve( + echo, + path=f"{os.environ['SUPERVISOR_PROCESS_NAME']}.sock", + ): + await stop + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/deployment/nginx/nginx.conf b/tests/wpt/tests/tools/third_party/websockets/example/deployment/nginx/nginx.conf new file mode 100644 index 00000000000..67aa0086d54 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/deployment/nginx/nginx.conf @@ -0,0 +1,25 @@ +daemon off; + +events { +} + +http { + server { + listen localhost:8080; + + location / { + proxy_http_version 1.1; + proxy_pass http://websocket; + proxy_set_header Connection $http_connection; + proxy_set_header Upgrade $http_upgrade; + } + } + + upstream websocket { + least_conn; + server unix:websockets-test_00.sock; + server unix:websockets-test_01.sock; + server unix:websockets-test_02.sock; + server unix:websockets-test_03.sock; + } +} diff --git a/tests/wpt/tests/tools/third_party/websockets/example/deployment/nginx/supervisord.conf b/tests/wpt/tests/tools/third_party/websockets/example/deployment/nginx/supervisord.conf new file mode 100644 index 00000000000..76a664d91b1 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/deployment/nginx/supervisord.conf @@ -0,0 +1,7 @@ +[supervisord] + +[program:websockets-test] +command = python app.py +process_name = %(program_name)s_%(process_num)02d +numprocs = 4 +autorestart = true diff --git a/tests/wpt/tests/tools/third_party/websockets/example/deployment/render/app.py b/tests/wpt/tests/tools/third_party/websockets/example/deployment/render/app.py new file mode 100644 index 00000000000..4ca34d23bbe --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/deployment/render/app.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python + +import asyncio +import http +import signal + +import websockets + + +async def echo(websocket): + async for message in websocket: + await websocket.send(message) + + +async def health_check(path, request_headers): + if path == "/healthz": + return http.HTTPStatus.OK, [], b"OK\n" + + +async def main(): + # Set the stop condition when receiving SIGTERM. + loop = asyncio.get_running_loop() + stop = loop.create_future() + loop.add_signal_handler(signal.SIGTERM, stop.set_result, None) + + async with websockets.serve( + echo, + host="", + port=8080, + process_request=health_check, + ): + await stop + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/deployment/render/requirements.txt b/tests/wpt/tests/tools/third_party/websockets/example/deployment/render/requirements.txt new file mode 100644 index 00000000000..14774b465e9 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/deployment/render/requirements.txt @@ -0,0 +1 @@ +websockets diff --git a/tests/wpt/tests/tools/third_party/websockets/example/deployment/supervisor/app.py b/tests/wpt/tests/tools/third_party/websockets/example/deployment/supervisor/app.py new file mode 100644 index 00000000000..bf61983ef7b --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/deployment/supervisor/app.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +import asyncio +import signal + +import websockets + + +async def echo(websocket): + async for message in websocket: + await websocket.send(message) + + +async def main(): + # Set the stop condition when receiving SIGTERM. + loop = asyncio.get_running_loop() + stop = loop.create_future() + loop.add_signal_handler(signal.SIGTERM, stop.set_result, None) + + async with websockets.serve( + echo, + host="", + port=8080, + reuse_port=True, + ): + await stop + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/deployment/supervisor/supervisord.conf b/tests/wpt/tests/tools/third_party/websockets/example/deployment/supervisor/supervisord.conf new file mode 100644 index 00000000000..76a664d91b1 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/deployment/supervisor/supervisord.conf @@ -0,0 +1,7 @@ +[supervisord] + +[program:websockets-test] +command = python app.py +process_name = %(program_name)s_%(process_num)02d +numprocs = 4 +autorestart = true diff --git a/tests/wpt/tests/tools/third_party/websockets/example/django/authentication.py b/tests/wpt/tests/tools/third_party/websockets/example/django/authentication.py new file mode 100644 index 00000000000..f6dad0f55ef --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/django/authentication.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python + +import asyncio + +import django +import websockets + +django.setup() + +from sesame.utils import get_user +from websockets.frames import CloseCode + + +async def handler(websocket): + sesame = await websocket.recv() + user = await asyncio.to_thread(get_user, sesame) + if user is None: + await websocket.close(CloseCode.INTERNAL_ERROR, "authentication failed") + return + + await websocket.send(f"Hello {user}!") + + +async def main(): + async with websockets.serve(handler, "localhost", 8888): + await asyncio.Future() # run forever + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/django/notifications.py b/tests/wpt/tests/tools/third_party/websockets/example/django/notifications.py new file mode 100644 index 00000000000..3a9ed10cf09 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/django/notifications.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python + +import asyncio +import json + +import aioredis +import django +import websockets + +django.setup() + +from django.contrib.contenttypes.models import ContentType +from sesame.utils import get_user +from websockets.frames import CloseCode + + +CONNECTIONS = {} + + +def get_content_types(user): + """Return the set of IDs of content types visible by user.""" + # This does only three database queries because Django caches + # all permissions on the first call to user.has_perm(...). + return { + ct.id + for ct in ContentType.objects.all() + if user.has_perm(f"{ct.app_label}.view_{ct.model}") + or user.has_perm(f"{ct.app_label}.change_{ct.model}") + } + + +async def handler(websocket): + """Authenticate user and register connection in CONNECTIONS.""" + sesame = await websocket.recv() + user = await asyncio.to_thread(get_user, sesame) + if user is None: + await websocket.close(CloseCode.INTERNAL_ERROR, "authentication failed") + return + + ct_ids = await asyncio.to_thread(get_content_types, user) + CONNECTIONS[websocket] = {"content_type_ids": ct_ids} + try: + await websocket.wait_closed() + finally: + del CONNECTIONS[websocket] + + +async def process_events(): + """Listen to events in Redis and process them.""" + redis = aioredis.from_url("redis://127.0.0.1:6379/1") + pubsub = redis.pubsub() + await pubsub.subscribe("events") + async for message in pubsub.listen(): + if message["type"] != "message": + continue + payload = message["data"].decode() + # Broadcast event to all users who have permissions to see it. + event = json.loads(payload) + recipients = ( + websocket + for websocket, connection in CONNECTIONS.items() + if event["content_type_id"] in connection["content_type_ids"] + ) + websockets.broadcast(recipients, payload) + + +async def main(): + async with websockets.serve(handler, "localhost", 8888): + await process_events() # runs forever + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/django/signals.py b/tests/wpt/tests/tools/third_party/websockets/example/django/signals.py new file mode 100644 index 00000000000..6dc827f72d7 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/django/signals.py @@ -0,0 +1,23 @@ +import json + +from django.contrib.admin.models import LogEntry +from django.db.models.signals import post_save +from django.dispatch import receiver + +from django_redis import get_redis_connection + + +@receiver(post_save, sender=LogEntry) +def publish_event(instance, **kwargs): + event = { + "model": instance.content_type.name, + "object": instance.object_repr, + "message": instance.get_change_message(), + "timestamp": instance.action_time.isoformat(), + "user": str(instance.user), + "content_type_id": instance.content_type_id, + "object_id": instance.object_id, + } + connection = get_redis_connection("default") + payload = json.dumps(event) + connection.publish("events", payload) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/echo.py b/tests/wpt/tests/tools/third_party/websockets/example/echo.py new file mode 100755 index 00000000000..2e47e52d949 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/echo.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python + +import asyncio +from websockets.server import serve + +async def echo(websocket): + async for message in websocket: + await websocket.send(message) + +async def main(): + async with serve(echo, "localhost", 8765): + await asyncio.Future() # run forever + +asyncio.run(main()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/faq/health_check_server.py b/tests/wpt/tests/tools/third_party/websockets/example/faq/health_check_server.py new file mode 100755 index 00000000000..7b8bded7720 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/faq/health_check_server.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python + +import asyncio +import http +import websockets + +async def health_check(path, request_headers): + if path == "/healthz": + return http.HTTPStatus.OK, [], b"OK\n" + +async def echo(websocket): + async for message in websocket: + await websocket.send(message) + +async def main(): + async with websockets.serve( + echo, "localhost", 8765, + process_request=health_check, + ): + await asyncio.Future() # run forever + +asyncio.run(main()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/faq/shutdown_client.py b/tests/wpt/tests/tools/third_party/websockets/example/faq/shutdown_client.py new file mode 100755 index 00000000000..539dd0304a9 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/faq/shutdown_client.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +import asyncio +import signal +import websockets + +async def client(): + uri = "ws://localhost:8765" + async with websockets.connect(uri) as websocket: + # Close the connection when receiving SIGTERM. + loop = asyncio.get_running_loop() + loop.add_signal_handler( + signal.SIGTERM, loop.create_task, websocket.close()) + + # Process messages received on the connection. + async for message in websocket: + ... + +asyncio.run(client()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/faq/shutdown_server.py b/tests/wpt/tests/tools/third_party/websockets/example/faq/shutdown_server.py new file mode 100755 index 00000000000..1bcc9c90baa --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/faq/shutdown_server.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +import asyncio +import signal +import websockets + +async def echo(websocket): + async for message in websocket: + await websocket.send(message) + +async def server(): + # Set the stop condition when receiving SIGTERM. + loop = asyncio.get_running_loop() + stop = loop.create_future() + loop.add_signal_handler(signal.SIGTERM, stop.set_result, None) + + async with websockets.serve(echo, "localhost", 8765): + await stop + +asyncio.run(server()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/hello.py b/tests/wpt/tests/tools/third_party/websockets/example/hello.py new file mode 100755 index 00000000000..a3ce0699ee4 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/hello.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +import asyncio +from websockets.sync.client import connect + +def hello(): + with connect("ws://localhost:8765") as websocket: + websocket.send("Hello world!") + message = websocket.recv() + print(f"Received: {message}") + +hello() diff --git a/tests/wpt/tests/tools/third_party/websockets/example/legacy/basic_auth_client.py b/tests/wpt/tests/tools/third_party/websockets/example/legacy/basic_auth_client.py new file mode 100755 index 00000000000..164732152f6 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/legacy/basic_auth_client.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python + +# WS client example with HTTP Basic Authentication + +import asyncio +import websockets + +async def hello(): + uri = "ws://mary:p@ssw0rd@localhost:8765" + async with websockets.connect(uri) as websocket: + greeting = await websocket.recv() + print(greeting) + +asyncio.run(hello()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/legacy/basic_auth_server.py b/tests/wpt/tests/tools/third_party/websockets/example/legacy/basic_auth_server.py new file mode 100755 index 00000000000..d2efeb7e530 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/legacy/basic_auth_server.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python + +# Server example with HTTP Basic Authentication over TLS + +import asyncio +import websockets + +async def hello(websocket): + greeting = f"Hello {websocket.username}!" + await websocket.send(greeting) + +async def main(): + async with websockets.serve( + hello, "localhost", 8765, + create_protocol=websockets.basic_auth_protocol_factory( + realm="example", credentials=("mary", "p@ssw0rd") + ), + ): + await asyncio.Future() # run forever + +asyncio.run(main()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/legacy/unix_client.py b/tests/wpt/tests/tools/third_party/websockets/example/legacy/unix_client.py new file mode 100755 index 00000000000..92615673032 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/legacy/unix_client.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +# WS client example connecting to a Unix socket + +import asyncio +import os.path +import websockets + +async def hello(): + socket_path = os.path.join(os.path.dirname(__file__), "socket") + async with websockets.unix_connect(socket_path) as websocket: + name = input("What's your name? ") + await websocket.send(name) + print(f">>> {name}") + + greeting = await websocket.recv() + print(f"<<< {greeting}") + +asyncio.run(hello()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/legacy/unix_server.py b/tests/wpt/tests/tools/third_party/websockets/example/legacy/unix_server.py new file mode 100755 index 00000000000..335039c351c --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/legacy/unix_server.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python + +# WS server example listening on a Unix socket + +import asyncio +import os.path +import websockets + +async def hello(websocket): + name = await websocket.recv() + print(f"<<< {name}") + + greeting = f"Hello {name}!" + + await websocket.send(greeting) + print(f">>> {greeting}") + +async def main(): + socket_path = os.path.join(os.path.dirname(__file__), "socket") + async with websockets.unix_serve(hello, socket_path): + await asyncio.Future() # run forever + +asyncio.run(main()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/logging/json_log_formatter.py b/tests/wpt/tests/tools/third_party/websockets/example/logging/json_log_formatter.py new file mode 100644 index 00000000000..b8fc8d6dc93 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/logging/json_log_formatter.py @@ -0,0 +1,33 @@ +import json +import logging +import datetime + +class JSONFormatter(logging.Formatter): + """ + Render logs as JSON. + + To add details to a log record, store them in a ``event_data`` + custom attribute. This dict is merged into the event. + + """ + def __init__(self): + pass # override logging.Formatter constructor + + def format(self, record): + event = { + "timestamp": self.getTimestamp(record.created), + "message": record.getMessage(), + "level": record.levelname, + "logger": record.name, + } + event_data = getattr(record, "event_data", None) + if event_data: + event.update(event_data) + if record.exc_info: + event["exc_info"] = self.formatException(record.exc_info) + if record.stack_info: + event["stack_info"] = self.formatStack(record.stack_info) + return json.dumps(event) + + def getTimestamp(self, created): + return datetime.datetime.utcfromtimestamp(created).isoformat() diff --git a/tests/wpt/tests/tools/third_party/websockets/example/quickstart/client.py b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/client.py new file mode 100755 index 00000000000..8d588c2b0e4 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/client.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +import asyncio +import websockets + +async def hello(): + uri = "ws://localhost:8765" + async with websockets.connect(uri) as websocket: + name = input("What's your name? ") + + await websocket.send(name) + print(f">>> {name}") + + greeting = await websocket.recv() + print(f"<<< {greeting}") + +if __name__ == "__main__": + asyncio.run(hello()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/quickstart/client_secure.py b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/client_secure.py new file mode 100755 index 00000000000..f4b39f2b838 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/client_secure.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python + +import asyncio +import pathlib +import ssl +import websockets + +ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) +localhost_pem = pathlib.Path(__file__).with_name("localhost.pem") +ssl_context.load_verify_locations(localhost_pem) + +async def hello(): + uri = "wss://localhost:8765" + async with websockets.connect(uri, ssl=ssl_context) as websocket: + name = input("What's your name? ") + + await websocket.send(name) + print(f">>> {name}") + + greeting = await websocket.recv() + print(f"<<< {greeting}") + +if __name__ == "__main__": + asyncio.run(hello()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/quickstart/counter.css b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/counter.css new file mode 100644 index 00000000000..e1f4b77148b --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/counter.css @@ -0,0 +1,33 @@ +body { + font-family: "Courier New", sans-serif; + text-align: center; +} +.buttons { + font-size: 4em; + display: flex; + justify-content: center; +} +.button, .value { + line-height: 1; + padding: 2rem; + margin: 2rem; + border: medium solid; + min-height: 1em; + min-width: 1em; +} +.button { + cursor: pointer; + user-select: none; +} +.minus { + color: red; +} +.plus { + color: green; +} +.value { + min-width: 2em; +} +.state { + font-size: 2em; +} diff --git a/tests/wpt/tests/tools/third_party/websockets/example/quickstart/counter.html b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/counter.html new file mode 100644 index 00000000000..2e3433bd215 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/counter.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>WebSocket demo</title> + <link href="counter.css" rel="stylesheet"> + </head> + <body> + <div class="buttons"> + <div class="minus button">-</div> + <div class="value">?</div> + <div class="plus button">+</div> + </div> + <div class="state"> + <span class="users">?</span> online + </div> + <script src="counter.js"></script> + </body> +</html> diff --git a/tests/wpt/tests/tools/third_party/websockets/example/quickstart/counter.js b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/counter.js new file mode 100644 index 00000000000..37d892a28b4 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/counter.js @@ -0,0 +1,26 @@ +window.addEventListener("DOMContentLoaded", () => { + const websocket = new WebSocket("ws://localhost:6789/"); + + document.querySelector(".minus").addEventListener("click", () => { + websocket.send(JSON.stringify({ action: "minus" })); + }); + + document.querySelector(".plus").addEventListener("click", () => { + websocket.send(JSON.stringify({ action: "plus" })); + }); + + websocket.onmessage = ({ data }) => { + const event = JSON.parse(data); + switch (event.type) { + case "value": + document.querySelector(".value").textContent = event.value; + break; + case "users": + const users = `${event.count} user${event.count == 1 ? "" : "s"}`; + document.querySelector(".users").textContent = users; + break; + default: + console.error("unsupported event", event); + } + }; +}); diff --git a/tests/wpt/tests/tools/third_party/websockets/example/quickstart/counter.py b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/counter.py new file mode 100755 index 00000000000..566e12965ed --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/counter.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python + +import asyncio +import json +import logging +import websockets + +logging.basicConfig() + +USERS = set() + +VALUE = 0 + +def users_event(): + return json.dumps({"type": "users", "count": len(USERS)}) + +def value_event(): + return json.dumps({"type": "value", "value": VALUE}) + +async def counter(websocket): + global USERS, VALUE + try: + # Register user + USERS.add(websocket) + websockets.broadcast(USERS, users_event()) + # Send current state to user + await websocket.send(value_event()) + # Manage state changes + async for message in websocket: + event = json.loads(message) + if event["action"] == "minus": + VALUE -= 1 + websockets.broadcast(USERS, value_event()) + elif event["action"] == "plus": + VALUE += 1 + websockets.broadcast(USERS, value_event()) + else: + logging.error("unsupported event: %s", event) + finally: + # Unregister user + USERS.remove(websocket) + websockets.broadcast(USERS, users_event()) + +async def main(): + async with websockets.serve(counter, "localhost", 6789): + await asyncio.Future() # run forever + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/quickstart/localhost.pem b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/localhost.pem new file mode 100644 index 00000000000..f9a30ba8f63 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/localhost.pem @@ -0,0 +1,48 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDG8iDak4UBpurI +TWjSfqJ0YVG/S56nhswehupCaIzu0xQ8wqPSs36h5t1jMexJPZfvwyvFjcV+hYpj +LMM0wMJPx9oBQEe0bsmlC66e8aF0UpSQw1aVfYoxA9BejgEyrFNE7cRbQNYFEb/5 +3HfqZKdEQA2fgQSlZ0RTRmLrD+l72iO5o2xl5bttXpqYZB2XOkyO79j/xWdu9zFE +sgZJ5ysWbqoRAGgnxjdYYr9DARd8bIE/hN3SW7mDt5v4LqCIhGn1VmrwtT3d5AuG +QPz4YEbm0t6GOlmFjIMYH5Y7pALRVfoJKRj6DGNIR1JicL+wqLV66kcVnj8WKbla +20i7fR7NAgMBAAECggEAG5yvgqbG5xvLqlFUIyMAWTbIqcxNEONcoUAIc38fUGZr +gKNjKXNQOBha0dG0AdZSqCxmftzWdGEEfA9SaJf4YCpUz6ekTB60Tfv5GIZg6kwr +4ou6ELWD4Jmu6fC7qdTRGdgGUMQG8F0uT/eRjS67KHXbbi/x/SMAEK7MO+PRfCbj ++JGzS9Ym9mUweINPotgjHdDGwwd039VWYS+9A+QuNK27p3zq4hrWRb4wshSC8fKy +oLoe4OQt81aowpX9k6mAU6N8vOmP8/EcQHYC+yFIIDZB2EmDP07R1LUEH3KJnzo7 +plCK1/kYPhX0a05cEdTpXdKa74AlvSRkS11sGqfUAQKBgQDj1SRv0AUGsHSA0LWx +a0NT1ZLEXCG0uqgdgh0sTqIeirQsPROw3ky4lH5MbjkfReArFkhHu3M6KoywEPxE +wanSRh/t1qcNjNNZUvFoUzAKVpb33RLkJppOTVEWPt+wtyDlfz1ZAXzMV66tACrx +H2a3v0ZWUz6J+x/dESH5TTNL4QKBgQDfirmknp408pwBE+bulngKy0QvU09En8H0 +uvqr8q4jCXqJ1tXon4wsHg2yF4Fa37SCpSmvONIDwJvVWkkYLyBHKOns/fWCkW3n +hIcYx0q2jgcoOLU0uoaM9ArRXhIxoWqV/KGkQzN+3xXC1/MxZ5OhyxBxfPCPIYIN +YN3M1t/QbQKBgDImhsC+D30rdlmsl3IYZFed2ZKznQ/FTqBANd+8517FtWdPgnga +VtUCitKUKKrDnNafLwXrMzAIkbNn6b/QyWrp2Lln2JnY9+TfpxgJx7de3BhvZ2sl +PC4kQsccy+yAQxOBcKWY+Dmay251bP5qpRepWPhDlq6UwqzMyqev4KzBAoGAWDMi +IEO9ZGK9DufNXCHeZ1PgKVQTmJ34JxmHQkTUVFqvEKfFaq1Y3ydUfAouLa7KSCnm +ko42vuhGFB41bOdbMvh/o9RoBAZheNGfhDVN002ioUoOpSlbYU4A3q7hOtfXeCpf +lLI3JT3cFi6ic8HMTDAU4tJLEA5GhATOPr4hPNkCgYB8jTYGcLvoeFaLEveg0kS2 +cz6ZXGLJx5m1AOQy5g9FwGaW+10lr8TF2k3AldwoiwX0R6sHAf/945aGU83ms5v9 +PB9/x66AYtSRUos9MwB4y1ur4g6FiXZUBgTJUqzz2nehPCyGjYhh49WucjszqcjX +chS1bKZOY+1knWq8xj5Qyg== +-----END PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDTTCCAjWgAwIBAgIJAOjte6l+03jvMA0GCSqGSIb3DQEBCwUAMEwxCzAJBgNV +BAYTAkZSMQ4wDAYDVQQHDAVQYXJpczEZMBcGA1UECgwQQXltZXJpYyBBdWd1c3Rp +bjESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTE4MDUwNTE2NTkyOVoYDzIwNjAwNTA0 +MTY1OTI5WjBMMQswCQYDVQQGEwJGUjEOMAwGA1UEBwwFUGFyaXMxGTAXBgNVBAoM +EEF5bWVyaWMgQXVndXN0aW4xEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMbyINqThQGm6shNaNJ+onRhUb9LnqeGzB6G +6kJojO7TFDzCo9KzfqHm3WMx7Ek9l+/DK8WNxX6FimMswzTAwk/H2gFAR7RuyaUL +rp7xoXRSlJDDVpV9ijED0F6OATKsU0TtxFtA1gURv/ncd+pkp0RADZ+BBKVnRFNG +YusP6XvaI7mjbGXlu21emphkHZc6TI7v2P/FZ273MUSyBknnKxZuqhEAaCfGN1hi +v0MBF3xsgT+E3dJbuYO3m/guoIiEafVWavC1Pd3kC4ZA/PhgRubS3oY6WYWMgxgf +ljukAtFV+gkpGPoMY0hHUmJwv7CotXrqRxWePxYpuVrbSLt9Hs0CAwEAAaMwMC4w +LAYDVR0RBCUwI4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0G +CSqGSIb3DQEBCwUAA4IBAQC9TsTxTEvqHPUS6sfvF77eG0D6HLOONVN91J+L7LiX +v3bFeS1xbUS6/wIxZi5EnAt/te5vaHk/5Q1UvznQP4j2gNoM6lH/DRkSARvRitVc +H0qN4Xp2Yk1R9VEx4ZgArcyMpI+GhE4vJRx1LE/hsuAzw7BAdsTt9zicscNg2fxO +3ao/eBcdaC6n9aFYdE6CADMpB1lCX2oWNVdj6IavQLu7VMc+WJ3RKncwC9th+5OP +ISPvkVZWf25rR2STmvvb0qEm3CZjk4Xd7N+gxbKKUvzEgPjrLSWzKKJAWHjCLugI +/kQqhpjWVlTbtKzWz5bViqCjSbrIPpU2MgG9AUV9y3iV +-----END CERTIFICATE----- diff --git a/tests/wpt/tests/tools/third_party/websockets/example/quickstart/server.py b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/server.py new file mode 100755 index 00000000000..31b18297298 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/server.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +import asyncio +import websockets + +async def hello(websocket): + name = await websocket.recv() + print(f"<<< {name}") + + greeting = f"Hello {name}!" + + await websocket.send(greeting) + print(f">>> {greeting}") + +async def main(): + async with websockets.serve(hello, "localhost", 8765): + await asyncio.Future() # run forever + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/quickstart/server_secure.py b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/server_secure.py new file mode 100755 index 00000000000..de41d30dc05 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/server_secure.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python + +import asyncio +import pathlib +import ssl +import websockets + +async def hello(websocket): + name = await websocket.recv() + print(f"<<< {name}") + + greeting = f"Hello {name}!" + + await websocket.send(greeting) + print(f">>> {greeting}") + +ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) +localhost_pem = pathlib.Path(__file__).with_name("localhost.pem") +ssl_context.load_cert_chain(localhost_pem) + +async def main(): + async with websockets.serve(hello, "localhost", 8765, ssl=ssl_context): + await asyncio.Future() # run forever + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/quickstart/show_time.html b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/show_time.html new file mode 100644 index 00000000000..b1c93b141d2 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/show_time.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>WebSocket demo</title> + </head> + <body> + <script src="show_time.js"></script> + </body> +</html> diff --git a/tests/wpt/tests/tools/third_party/websockets/example/quickstart/show_time.js b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/show_time.js new file mode 100644 index 00000000000..26bed7ec9e7 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/show_time.js @@ -0,0 +1,12 @@ +window.addEventListener("DOMContentLoaded", () => { + const messages = document.createElement("ul"); + document.body.appendChild(messages); + + const websocket = new WebSocket("ws://localhost:5678/"); + websocket.onmessage = ({ data }) => { + const message = document.createElement("li"); + const content = document.createTextNode(data); + message.appendChild(content); + messages.appendChild(message); + }; +}); diff --git a/tests/wpt/tests/tools/third_party/websockets/example/quickstart/show_time.py b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/show_time.py new file mode 100755 index 00000000000..a83078e8a91 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/show_time.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +import asyncio +import datetime +import random +import websockets + +async def show_time(websocket): + while True: + message = datetime.datetime.utcnow().isoformat() + "Z" + await websocket.send(message) + await asyncio.sleep(random.random() * 2 + 1) + +async def main(): + async with websockets.serve(show_time, "localhost", 5678): + await asyncio.Future() # run forever + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/quickstart/show_time_2.py b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/show_time_2.py new file mode 100755 index 00000000000..08e87f5931a --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/quickstart/show_time_2.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python + +import asyncio +import datetime +import random +import websockets + +CONNECTIONS = set() + +async def register(websocket): + CONNECTIONS.add(websocket) + try: + await websocket.wait_closed() + finally: + CONNECTIONS.remove(websocket) + +async def show_time(): + while True: + message = datetime.datetime.utcnow().isoformat() + "Z" + websockets.broadcast(CONNECTIONS, message) + await asyncio.sleep(random.random() * 2 + 1) + +async def main(): + async with websockets.serve(register, "localhost", 5678): + await show_time() + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/start/connect4.css b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/start/connect4.css new file mode 100644 index 00000000000..27f0baf6e45 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/start/connect4.css @@ -0,0 +1,105 @@ +/* General layout */ + +body { + background-color: white; + display: flex; + flex-direction: column-reverse; + justify-content: center; + align-items: center; + margin: 0; + min-height: 100vh; +} + +/* Action buttons */ + +.actions { + display: flex; + flex-direction: row; + justify-content: space-evenly; + align-items: flex-end; + width: 720px; + height: 100px; +} + +.action { + color: darkgray; + font-family: "Helvetica Neue", sans-serif; + font-size: 20px; + line-height: 20px; + font-weight: 300; + text-align: center; + text-decoration: none; + text-transform: uppercase; + padding: 20px; + width: 120px; +} + +.action:hover { + background-color: darkgray; + color: white; + font-weight: 700; +} + +.action[href=""] { + display: none; +} + +/* Connect Four board */ + +.board { + background-color: blue; + display: flex; + flex-direction: row; + padding: 0 10px; + position: relative; +} + +.board::before, +.board::after { + background-color: blue; + content: ""; + height: 720px; + width: 20px; + position: absolute; +} + +.board::before { + left: -20px; +} + +.board::after { + right: -20px; +} + +.column { + display: flex; + flex-direction: column-reverse; + padding: 10px; +} + +.cell { + border-radius: 50%; + width: 80px; + height: 80px; + margin: 10px 0; +} + +.empty { + background-color: white; +} + +.column:hover .empty { + background-color: lightgray; +} + +.column:hover .empty ~ .empty { + background-color: white; +} + +.red { + background-color: red; +} + +.yellow { + background-color: yellow; +} diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/start/connect4.js b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/start/connect4.js new file mode 100644 index 00000000000..cb5eb9fa27b --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/start/connect4.js @@ -0,0 +1,45 @@ +const PLAYER1 = "red"; + +const PLAYER2 = "yellow"; + +function createBoard(board) { + // Inject stylesheet. + const linkElement = document.createElement("link"); + linkElement.href = import.meta.url.replace(".js", ".css"); + linkElement.rel = "stylesheet"; + document.head.append(linkElement); + // Generate board. + for (let column = 0; column < 7; column++) { + const columnElement = document.createElement("div"); + columnElement.className = "column"; + columnElement.dataset.column = column; + for (let row = 0; row < 6; row++) { + const cellElement = document.createElement("div"); + cellElement.className = "cell empty"; + cellElement.dataset.column = column; + columnElement.append(cellElement); + } + board.append(columnElement); + } +} + +function playMove(board, player, column, row) { + // Check values of arguments. + if (player !== PLAYER1 && player !== PLAYER2) { + throw new Error(`player must be ${PLAYER1} or ${PLAYER2}.`); + } + const columnElement = board.querySelectorAll(".column")[column]; + if (columnElement === undefined) { + throw new RangeError("column must be between 0 and 6."); + } + const cellElement = columnElement.querySelectorAll(".cell")[row]; + if (cellElement === undefined) { + throw new RangeError("row must be between 0 and 5."); + } + // Place checker in cell. + if (!cellElement.classList.replace("empty", player)) { + throw new Error("cell must be empty."); + } +} + +export { PLAYER1, PLAYER2, createBoard, playMove }; diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/start/connect4.py b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/start/connect4.py new file mode 100644 index 00000000000..0a61e7c7ee1 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/start/connect4.py @@ -0,0 +1,62 @@ +__all__ = ["PLAYER1", "PLAYER2", "Connect4"] + +PLAYER1, PLAYER2 = "red", "yellow" + + +class Connect4: + """ + A Connect Four game. + + Play moves with :meth:`play`. + + Get past moves with :attr:`moves`. + + Check for a victory with :attr:`winner`. + + """ + + def __init__(self): + self.moves = [] + self.top = [0 for _ in range(7)] + self.winner = None + + @property + def last_player(self): + """ + Player who played the last move. + + """ + return PLAYER1 if len(self.moves) % 2 else PLAYER2 + + @property + def last_player_won(self): + """ + Whether the last move is winning. + + """ + b = sum(1 << (8 * column + row) for _, column, row in self.moves[::-2]) + return any(b & b >> v & b >> 2 * v & b >> 3 * v for v in [1, 7, 8, 9]) + + def play(self, player, column): + """ + Play a move in a column. + + Returns the row where the checker lands. + + Raises :exc:`RuntimeError` if the move is illegal. + + """ + if player == self.last_player: + raise RuntimeError("It isn't your turn.") + + row = self.top[column] + if row == 6: + raise RuntimeError("This slot is full.") + + self.moves.append((player, column, row)) + self.top[column] += 1 + + if self.winner is None and self.last_player_won: + self.winner = self.last_player + + return row diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/start/favicon.ico b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/start/favicon.ico Binary files differnew file mode 100644 index 00000000000..36e855029d7 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/start/favicon.ico diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step1/app.py b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step1/app.py new file mode 100644 index 00000000000..3b0fbd78685 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step1/app.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python + +import asyncio +import itertools +import json + +import websockets + +from connect4 import PLAYER1, PLAYER2, Connect4 + + +async def handler(websocket): + # Initialize a Connect Four game. + game = Connect4() + + # Players take alternate turns, using the same browser. + turns = itertools.cycle([PLAYER1, PLAYER2]) + player = next(turns) + + async for message in websocket: + # Parse a "play" event from the UI. + event = json.loads(message) + assert event["type"] == "play" + column = event["column"] + + try: + # Play the move. + row = game.play(player, column) + except RuntimeError as exc: + # Send an "error" event if the move was illegal. + event = { + "type": "error", + "message": str(exc), + } + await websocket.send(json.dumps(event)) + continue + + # Send a "play" event to update the UI. + event = { + "type": "play", + "player": player, + "column": column, + "row": row, + } + await websocket.send(json.dumps(event)) + + # If move is winning, send a "win" event. + if game.winner is not None: + event = { + "type": "win", + "player": game.winner, + } + await websocket.send(json.dumps(event)) + + # Alternate turns. + player = next(turns) + + +async def main(): + async with websockets.serve(handler, "", 8001): + await asyncio.Future() # run forever + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step1/connect4.css b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step1/connect4.css new file mode 100644 index 00000000000..27f0baf6e45 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step1/connect4.css @@ -0,0 +1,105 @@ +/* General layout */ + +body { + background-color: white; + display: flex; + flex-direction: column-reverse; + justify-content: center; + align-items: center; + margin: 0; + min-height: 100vh; +} + +/* Action buttons */ + +.actions { + display: flex; + flex-direction: row; + justify-content: space-evenly; + align-items: flex-end; + width: 720px; + height: 100px; +} + +.action { + color: darkgray; + font-family: "Helvetica Neue", sans-serif; + font-size: 20px; + line-height: 20px; + font-weight: 300; + text-align: center; + text-decoration: none; + text-transform: uppercase; + padding: 20px; + width: 120px; +} + +.action:hover { + background-color: darkgray; + color: white; + font-weight: 700; +} + +.action[href=""] { + display: none; +} + +/* Connect Four board */ + +.board { + background-color: blue; + display: flex; + flex-direction: row; + padding: 0 10px; + position: relative; +} + +.board::before, +.board::after { + background-color: blue; + content: ""; + height: 720px; + width: 20px; + position: absolute; +} + +.board::before { + left: -20px; +} + +.board::after { + right: -20px; +} + +.column { + display: flex; + flex-direction: column-reverse; + padding: 10px; +} + +.cell { + border-radius: 50%; + width: 80px; + height: 80px; + margin: 10px 0; +} + +.empty { + background-color: white; +} + +.column:hover .empty { + background-color: lightgray; +} + +.column:hover .empty ~ .empty { + background-color: white; +} + +.red { + background-color: red; +} + +.yellow { + background-color: yellow; +} diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step1/connect4.js b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step1/connect4.js new file mode 100644 index 00000000000..cb5eb9fa27b --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step1/connect4.js @@ -0,0 +1,45 @@ +const PLAYER1 = "red"; + +const PLAYER2 = "yellow"; + +function createBoard(board) { + // Inject stylesheet. + const linkElement = document.createElement("link"); + linkElement.href = import.meta.url.replace(".js", ".css"); + linkElement.rel = "stylesheet"; + document.head.append(linkElement); + // Generate board. + for (let column = 0; column < 7; column++) { + const columnElement = document.createElement("div"); + columnElement.className = "column"; + columnElement.dataset.column = column; + for (let row = 0; row < 6; row++) { + const cellElement = document.createElement("div"); + cellElement.className = "cell empty"; + cellElement.dataset.column = column; + columnElement.append(cellElement); + } + board.append(columnElement); + } +} + +function playMove(board, player, column, row) { + // Check values of arguments. + if (player !== PLAYER1 && player !== PLAYER2) { + throw new Error(`player must be ${PLAYER1} or ${PLAYER2}.`); + } + const columnElement = board.querySelectorAll(".column")[column]; + if (columnElement === undefined) { + throw new RangeError("column must be between 0 and 6."); + } + const cellElement = columnElement.querySelectorAll(".cell")[row]; + if (cellElement === undefined) { + throw new RangeError("row must be between 0 and 5."); + } + // Place checker in cell. + if (!cellElement.classList.replace("empty", player)) { + throw new Error("cell must be empty."); + } +} + +export { PLAYER1, PLAYER2, createBoard, playMove }; diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step1/connect4.py b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step1/connect4.py new file mode 100644 index 00000000000..0a61e7c7ee1 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step1/connect4.py @@ -0,0 +1,62 @@ +__all__ = ["PLAYER1", "PLAYER2", "Connect4"] + +PLAYER1, PLAYER2 = "red", "yellow" + + +class Connect4: + """ + A Connect Four game. + + Play moves with :meth:`play`. + + Get past moves with :attr:`moves`. + + Check for a victory with :attr:`winner`. + + """ + + def __init__(self): + self.moves = [] + self.top = [0 for _ in range(7)] + self.winner = None + + @property + def last_player(self): + """ + Player who played the last move. + + """ + return PLAYER1 if len(self.moves) % 2 else PLAYER2 + + @property + def last_player_won(self): + """ + Whether the last move is winning. + + """ + b = sum(1 << (8 * column + row) for _, column, row in self.moves[::-2]) + return any(b & b >> v & b >> 2 * v & b >> 3 * v for v in [1, 7, 8, 9]) + + def play(self, player, column): + """ + Play a move in a column. + + Returns the row where the checker lands. + + Raises :exc:`RuntimeError` if the move is illegal. + + """ + if player == self.last_player: + raise RuntimeError("It isn't your turn.") + + row = self.top[column] + if row == 6: + raise RuntimeError("This slot is full.") + + self.moves.append((player, column, row)) + self.top[column] += 1 + + if self.winner is None and self.last_player_won: + self.winner = self.last_player + + return row diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step1/favicon.ico b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step1/favicon.ico Binary files differnew file mode 100644 index 00000000000..36e855029d7 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step1/favicon.ico diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step1/index.html b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step1/index.html new file mode 100644 index 00000000000..8e38e899222 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step1/index.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>Connect Four</title> + </head> + <body> + <div class="board"></div> + <script src="main.js" type="module"></script> + </body> +</html> diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step1/main.js b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step1/main.js new file mode 100644 index 00000000000..dd28f9a6a8b --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step1/main.js @@ -0,0 +1,53 @@ +import { createBoard, playMove } from "./connect4.js"; + +function showMessage(message) { + window.setTimeout(() => window.alert(message), 50); +} + +function receiveMoves(board, websocket) { + websocket.addEventListener("message", ({ data }) => { + const event = JSON.parse(data); + switch (event.type) { + case "play": + // Update the UI with the move. + playMove(board, event.player, event.column, event.row); + break; + case "win": + showMessage(`Player ${event.player} wins!`); + // No further messages are expected; close the WebSocket connection. + websocket.close(1000); + break; + case "error": + showMessage(event.message); + break; + default: + throw new Error(`Unsupported event type: ${event.type}.`); + } + }); +} + +function sendMoves(board, websocket) { + // When clicking a column, send a "play" event for a move in that column. + board.addEventListener("click", ({ target }) => { + const column = target.dataset.column; + // Ignore clicks outside a column. + if (column === undefined) { + return; + } + const event = { + type: "play", + column: parseInt(column, 10), + }; + websocket.send(JSON.stringify(event)); + }); +} + +window.addEventListener("DOMContentLoaded", () => { + // Initialize the UI. + const board = document.querySelector(".board"); + createBoard(board); + // Open the WebSocket connection and register event handlers. + const websocket = new WebSocket("ws://localhost:8001/"); + receiveMoves(board, websocket); + sendMoves(board, websocket); +}); diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step2/app.py b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step2/app.py new file mode 100644 index 00000000000..2693d4304df --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step2/app.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python + +import asyncio +import json +import secrets + +import websockets + +from connect4 import PLAYER1, PLAYER2, Connect4 + + +JOIN = {} + +WATCH = {} + + +async def error(websocket, message): + """ + Send an error message. + + """ + event = { + "type": "error", + "message": message, + } + await websocket.send(json.dumps(event)) + + +async def replay(websocket, game): + """ + Send previous moves. + + """ + # Make a copy to avoid an exception if game.moves changes while iteration + # is in progress. If a move is played while replay is running, moves will + # be sent out of order but each move will be sent once and eventually the + # UI will be consistent. + for player, column, row in game.moves.copy(): + event = { + "type": "play", + "player": player, + "column": column, + "row": row, + } + await websocket.send(json.dumps(event)) + + +async def play(websocket, game, player, connected): + """ + Receive and process moves from a player. + + """ + async for message in websocket: + # Parse a "play" event from the UI. + event = json.loads(message) + assert event["type"] == "play" + column = event["column"] + + try: + # Play the move. + row = game.play(player, column) + except RuntimeError as exc: + # Send an "error" event if the move was illegal. + await error(websocket, str(exc)) + continue + + # Send a "play" event to update the UI. + event = { + "type": "play", + "player": player, + "column": column, + "row": row, + } + websockets.broadcast(connected, json.dumps(event)) + + # If move is winning, send a "win" event. + if game.winner is not None: + event = { + "type": "win", + "player": game.winner, + } + websockets.broadcast(connected, json.dumps(event)) + + +async def start(websocket): + """ + Handle a connection from the first player: start a new game. + + """ + # Initialize a Connect Four game, the set of WebSocket connections + # receiving moves from this game, and secret access tokens. + game = Connect4() + connected = {websocket} + + join_key = secrets.token_urlsafe(12) + JOIN[join_key] = game, connected + + watch_key = secrets.token_urlsafe(12) + WATCH[watch_key] = game, connected + + try: + # Send the secret access tokens to the browser of the first player, + # where they'll be used for building "join" and "watch" links. + event = { + "type": "init", + "join": join_key, + "watch": watch_key, + } + await websocket.send(json.dumps(event)) + # Receive and process moves from the first player. + await play(websocket, game, PLAYER1, connected) + finally: + del JOIN[join_key] + del WATCH[watch_key] + + +async def join(websocket, join_key): + """ + Handle a connection from the second player: join an existing game. + + """ + # Find the Connect Four game. + try: + game, connected = JOIN[join_key] + except KeyError: + await error(websocket, "Game not found.") + return + + # Register to receive moves from this game. + connected.add(websocket) + try: + # Send the first move, in case the first player already played it. + await replay(websocket, game) + # Receive and process moves from the second player. + await play(websocket, game, PLAYER2, connected) + finally: + connected.remove(websocket) + + +async def watch(websocket, watch_key): + """ + Handle a connection from a spectator: watch an existing game. + + """ + # Find the Connect Four game. + try: + game, connected = WATCH[watch_key] + except KeyError: + await error(websocket, "Game not found.") + return + + # Register to receive moves from this game. + connected.add(websocket) + try: + # Send previous moves, in case the game already started. + await replay(websocket, game) + # Keep the connection open, but don't receive any messages. + await websocket.wait_closed() + finally: + connected.remove(websocket) + + +async def handler(websocket): + """ + Handle a connection and dispatch it according to who is connecting. + + """ + # Receive and parse the "init" event from the UI. + message = await websocket.recv() + event = json.loads(message) + assert event["type"] == "init" + + if "join" in event: + # Second player joins an existing game. + await join(websocket, event["join"]) + elif "watch" in event: + # Spectator watches an existing game. + await watch(websocket, event["watch"]) + else: + # First player starts a new game. + await start(websocket) + + +async def main(): + async with websockets.serve(handler, "", 8001): + await asyncio.Future() # run forever + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step2/connect4.css b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step2/connect4.css new file mode 100644 index 00000000000..27f0baf6e45 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step2/connect4.css @@ -0,0 +1,105 @@ +/* General layout */ + +body { + background-color: white; + display: flex; + flex-direction: column-reverse; + justify-content: center; + align-items: center; + margin: 0; + min-height: 100vh; +} + +/* Action buttons */ + +.actions { + display: flex; + flex-direction: row; + justify-content: space-evenly; + align-items: flex-end; + width: 720px; + height: 100px; +} + +.action { + color: darkgray; + font-family: "Helvetica Neue", sans-serif; + font-size: 20px; + line-height: 20px; + font-weight: 300; + text-align: center; + text-decoration: none; + text-transform: uppercase; + padding: 20px; + width: 120px; +} + +.action:hover { + background-color: darkgray; + color: white; + font-weight: 700; +} + +.action[href=""] { + display: none; +} + +/* Connect Four board */ + +.board { + background-color: blue; + display: flex; + flex-direction: row; + padding: 0 10px; + position: relative; +} + +.board::before, +.board::after { + background-color: blue; + content: ""; + height: 720px; + width: 20px; + position: absolute; +} + +.board::before { + left: -20px; +} + +.board::after { + right: -20px; +} + +.column { + display: flex; + flex-direction: column-reverse; + padding: 10px; +} + +.cell { + border-radius: 50%; + width: 80px; + height: 80px; + margin: 10px 0; +} + +.empty { + background-color: white; +} + +.column:hover .empty { + background-color: lightgray; +} + +.column:hover .empty ~ .empty { + background-color: white; +} + +.red { + background-color: red; +} + +.yellow { + background-color: yellow; +} diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step2/connect4.js b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step2/connect4.js new file mode 100644 index 00000000000..cb5eb9fa27b --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step2/connect4.js @@ -0,0 +1,45 @@ +const PLAYER1 = "red"; + +const PLAYER2 = "yellow"; + +function createBoard(board) { + // Inject stylesheet. + const linkElement = document.createElement("link"); + linkElement.href = import.meta.url.replace(".js", ".css"); + linkElement.rel = "stylesheet"; + document.head.append(linkElement); + // Generate board. + for (let column = 0; column < 7; column++) { + const columnElement = document.createElement("div"); + columnElement.className = "column"; + columnElement.dataset.column = column; + for (let row = 0; row < 6; row++) { + const cellElement = document.createElement("div"); + cellElement.className = "cell empty"; + cellElement.dataset.column = column; + columnElement.append(cellElement); + } + board.append(columnElement); + } +} + +function playMove(board, player, column, row) { + // Check values of arguments. + if (player !== PLAYER1 && player !== PLAYER2) { + throw new Error(`player must be ${PLAYER1} or ${PLAYER2}.`); + } + const columnElement = board.querySelectorAll(".column")[column]; + if (columnElement === undefined) { + throw new RangeError("column must be between 0 and 6."); + } + const cellElement = columnElement.querySelectorAll(".cell")[row]; + if (cellElement === undefined) { + throw new RangeError("row must be between 0 and 5."); + } + // Place checker in cell. + if (!cellElement.classList.replace("empty", player)) { + throw new Error("cell must be empty."); + } +} + +export { PLAYER1, PLAYER2, createBoard, playMove }; diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step2/connect4.py b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step2/connect4.py new file mode 100644 index 00000000000..0a61e7c7ee1 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step2/connect4.py @@ -0,0 +1,62 @@ +__all__ = ["PLAYER1", "PLAYER2", "Connect4"] + +PLAYER1, PLAYER2 = "red", "yellow" + + +class Connect4: + """ + A Connect Four game. + + Play moves with :meth:`play`. + + Get past moves with :attr:`moves`. + + Check for a victory with :attr:`winner`. + + """ + + def __init__(self): + self.moves = [] + self.top = [0 for _ in range(7)] + self.winner = None + + @property + def last_player(self): + """ + Player who played the last move. + + """ + return PLAYER1 if len(self.moves) % 2 else PLAYER2 + + @property + def last_player_won(self): + """ + Whether the last move is winning. + + """ + b = sum(1 << (8 * column + row) for _, column, row in self.moves[::-2]) + return any(b & b >> v & b >> 2 * v & b >> 3 * v for v in [1, 7, 8, 9]) + + def play(self, player, column): + """ + Play a move in a column. + + Returns the row where the checker lands. + + Raises :exc:`RuntimeError` if the move is illegal. + + """ + if player == self.last_player: + raise RuntimeError("It isn't your turn.") + + row = self.top[column] + if row == 6: + raise RuntimeError("This slot is full.") + + self.moves.append((player, column, row)) + self.top[column] += 1 + + if self.winner is None and self.last_player_won: + self.winner = self.last_player + + return row diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step2/favicon.ico b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step2/favicon.ico Binary files differnew file mode 100644 index 00000000000..36e855029d7 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step2/favicon.ico diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step2/index.html b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step2/index.html new file mode 100644 index 00000000000..1a16f72a257 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step2/index.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>Connect Four</title> + </head> + <body> + <div class="actions"> + <a class="action new" href="/">New</a> + <a class="action join" href="">Join</a> + <a class="action watch" href="">Watch</a> + </div> + <div class="board"></div> + <script src="main.js" type="module"></script> + </body> +</html> diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step2/main.js b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step2/main.js new file mode 100644 index 00000000000..d38a0140acc --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step2/main.js @@ -0,0 +1,83 @@ +import { createBoard, playMove } from "./connect4.js"; + +function initGame(websocket) { + websocket.addEventListener("open", () => { + // Send an "init" event according to who is connecting. + const params = new URLSearchParams(window.location.search); + let event = { type: "init" }; + if (params.has("join")) { + // Second player joins an existing game. + event.join = params.get("join"); + } else if (params.has("watch")) { + // Spectator watches an existing game. + event.watch = params.get("watch"); + } else { + // First player starts a new game. + } + websocket.send(JSON.stringify(event)); + }); +} + +function showMessage(message) { + window.setTimeout(() => window.alert(message), 50); +} + +function receiveMoves(board, websocket) { + websocket.addEventListener("message", ({ data }) => { + const event = JSON.parse(data); + switch (event.type) { + case "init": + // Create links for inviting the second player and spectators. + document.querySelector(".join").href = "?join=" + event.join; + document.querySelector(".watch").href = "?watch=" + event.watch; + break; + case "play": + // Update the UI with the move. + playMove(board, event.player, event.column, event.row); + break; + case "win": + showMessage(`Player ${event.player} wins!`); + // No further messages are expected; close the WebSocket connection. + websocket.close(1000); + break; + case "error": + showMessage(event.message); + break; + default: + throw new Error(`Unsupported event type: ${event.type}.`); + } + }); +} + +function sendMoves(board, websocket) { + // Don't send moves for a spectator watching a game. + const params = new URLSearchParams(window.location.search); + if (params.has("watch")) { + return; + } + + // When clicking a column, send a "play" event for a move in that column. + board.addEventListener("click", ({ target }) => { + const column = target.dataset.column; + // Ignore clicks outside a column. + if (column === undefined) { + return; + } + const event = { + type: "play", + column: parseInt(column, 10), + }; + websocket.send(JSON.stringify(event)); + }); +} + +window.addEventListener("DOMContentLoaded", () => { + // Initialize the UI. + const board = document.querySelector(".board"); + createBoard(board); + // Open the WebSocket connection and register event handlers. + const websocket = new WebSocket("ws://localhost:8001/"); + initGame(websocket); + receiveMoves(board, websocket); + sendMoves(board, websocket); +}); diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/Procfile b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/Procfile new file mode 100644 index 00000000000..2e35818f675 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/Procfile @@ -0,0 +1 @@ +web: python app.py diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/app.py b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/app.py new file mode 100644 index 00000000000..c2ee020d201 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/app.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python + +import asyncio +import json +import os +import secrets +import signal + +import websockets + +from connect4 import PLAYER1, PLAYER2, Connect4 + + +JOIN = {} + +WATCH = {} + + +async def error(websocket, message): + """ + Send an error message. + + """ + event = { + "type": "error", + "message": message, + } + await websocket.send(json.dumps(event)) + + +async def replay(websocket, game): + """ + Send previous moves. + + """ + # Make a copy to avoid an exception if game.moves changes while iteration + # is in progress. If a move is played while replay is running, moves will + # be sent out of order but each move will be sent once and eventually the + # UI will be consistent. + for player, column, row in game.moves.copy(): + event = { + "type": "play", + "player": player, + "column": column, + "row": row, + } + await websocket.send(json.dumps(event)) + + +async def play(websocket, game, player, connected): + """ + Receive and process moves from a player. + + """ + async for message in websocket: + # Parse a "play" event from the UI. + event = json.loads(message) + assert event["type"] == "play" + column = event["column"] + + try: + # Play the move. + row = game.play(player, column) + except RuntimeError as exc: + # Send an "error" event if the move was illegal. + await error(websocket, str(exc)) + continue + + # Send a "play" event to update the UI. + event = { + "type": "play", + "player": player, + "column": column, + "row": row, + } + websockets.broadcast(connected, json.dumps(event)) + + # If move is winning, send a "win" event. + if game.winner is not None: + event = { + "type": "win", + "player": game.winner, + } + websockets.broadcast(connected, json.dumps(event)) + + +async def start(websocket): + """ + Handle a connection from the first player: start a new game. + + """ + # Initialize a Connect Four game, the set of WebSocket connections + # receiving moves from this game, and secret access tokens. + game = Connect4() + connected = {websocket} + + join_key = secrets.token_urlsafe(12) + JOIN[join_key] = game, connected + + watch_key = secrets.token_urlsafe(12) + WATCH[watch_key] = game, connected + + try: + # Send the secret access tokens to the browser of the first player, + # where they'll be used for building "join" and "watch" links. + event = { + "type": "init", + "join": join_key, + "watch": watch_key, + } + await websocket.send(json.dumps(event)) + # Receive and process moves from the first player. + await play(websocket, game, PLAYER1, connected) + finally: + del JOIN[join_key] + del WATCH[watch_key] + + +async def join(websocket, join_key): + """ + Handle a connection from the second player: join an existing game. + + """ + # Find the Connect Four game. + try: + game, connected = JOIN[join_key] + except KeyError: + await error(websocket, "Game not found.") + return + + # Register to receive moves from this game. + connected.add(websocket) + try: + # Send the first move, in case the first player already played it. + await replay(websocket, game) + # Receive and process moves from the second player. + await play(websocket, game, PLAYER2, connected) + finally: + connected.remove(websocket) + + +async def watch(websocket, watch_key): + """ + Handle a connection from a spectator: watch an existing game. + + """ + # Find the Connect Four game. + try: + game, connected = WATCH[watch_key] + except KeyError: + await error(websocket, "Game not found.") + return + + # Register to receive moves from this game. + connected.add(websocket) + try: + # Send previous moves, in case the game already started. + await replay(websocket, game) + # Keep the connection open, but don't receive any messages. + await websocket.wait_closed() + finally: + connected.remove(websocket) + + +async def handler(websocket): + """ + Handle a connection and dispatch it according to who is connecting. + + """ + # Receive and parse the "init" event from the UI. + message = await websocket.recv() + event = json.loads(message) + assert event["type"] == "init" + + if "join" in event: + # Second player joins an existing game. + await join(websocket, event["join"]) + elif "watch" in event: + # Spectator watches an existing game. + await watch(websocket, event["watch"]) + else: + # First player starts a new game. + await start(websocket) + + +async def main(): + # Set the stop condition when receiving SIGTERM. + loop = asyncio.get_running_loop() + stop = loop.create_future() + loop.add_signal_handler(signal.SIGTERM, stop.set_result, None) + + port = int(os.environ.get("PORT", "8001")) + async with websockets.serve(handler, "", port): + await stop + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/connect4.css b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/connect4.css new file mode 100644 index 00000000000..27f0baf6e45 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/connect4.css @@ -0,0 +1,105 @@ +/* General layout */ + +body { + background-color: white; + display: flex; + flex-direction: column-reverse; + justify-content: center; + align-items: center; + margin: 0; + min-height: 100vh; +} + +/* Action buttons */ + +.actions { + display: flex; + flex-direction: row; + justify-content: space-evenly; + align-items: flex-end; + width: 720px; + height: 100px; +} + +.action { + color: darkgray; + font-family: "Helvetica Neue", sans-serif; + font-size: 20px; + line-height: 20px; + font-weight: 300; + text-align: center; + text-decoration: none; + text-transform: uppercase; + padding: 20px; + width: 120px; +} + +.action:hover { + background-color: darkgray; + color: white; + font-weight: 700; +} + +.action[href=""] { + display: none; +} + +/* Connect Four board */ + +.board { + background-color: blue; + display: flex; + flex-direction: row; + padding: 0 10px; + position: relative; +} + +.board::before, +.board::after { + background-color: blue; + content: ""; + height: 720px; + width: 20px; + position: absolute; +} + +.board::before { + left: -20px; +} + +.board::after { + right: -20px; +} + +.column { + display: flex; + flex-direction: column-reverse; + padding: 10px; +} + +.cell { + border-radius: 50%; + width: 80px; + height: 80px; + margin: 10px 0; +} + +.empty { + background-color: white; +} + +.column:hover .empty { + background-color: lightgray; +} + +.column:hover .empty ~ .empty { + background-color: white; +} + +.red { + background-color: red; +} + +.yellow { + background-color: yellow; +} diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/connect4.js b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/connect4.js new file mode 100644 index 00000000000..cb5eb9fa27b --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/connect4.js @@ -0,0 +1,45 @@ +const PLAYER1 = "red"; + +const PLAYER2 = "yellow"; + +function createBoard(board) { + // Inject stylesheet. + const linkElement = document.createElement("link"); + linkElement.href = import.meta.url.replace(".js", ".css"); + linkElement.rel = "stylesheet"; + document.head.append(linkElement); + // Generate board. + for (let column = 0; column < 7; column++) { + const columnElement = document.createElement("div"); + columnElement.className = "column"; + columnElement.dataset.column = column; + for (let row = 0; row < 6; row++) { + const cellElement = document.createElement("div"); + cellElement.className = "cell empty"; + cellElement.dataset.column = column; + columnElement.append(cellElement); + } + board.append(columnElement); + } +} + +function playMove(board, player, column, row) { + // Check values of arguments. + if (player !== PLAYER1 && player !== PLAYER2) { + throw new Error(`player must be ${PLAYER1} or ${PLAYER2}.`); + } + const columnElement = board.querySelectorAll(".column")[column]; + if (columnElement === undefined) { + throw new RangeError("column must be between 0 and 6."); + } + const cellElement = columnElement.querySelectorAll(".cell")[row]; + if (cellElement === undefined) { + throw new RangeError("row must be between 0 and 5."); + } + // Place checker in cell. + if (!cellElement.classList.replace("empty", player)) { + throw new Error("cell must be empty."); + } +} + +export { PLAYER1, PLAYER2, createBoard, playMove }; diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/connect4.py b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/connect4.py new file mode 100644 index 00000000000..0a61e7c7ee1 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/connect4.py @@ -0,0 +1,62 @@ +__all__ = ["PLAYER1", "PLAYER2", "Connect4"] + +PLAYER1, PLAYER2 = "red", "yellow" + + +class Connect4: + """ + A Connect Four game. + + Play moves with :meth:`play`. + + Get past moves with :attr:`moves`. + + Check for a victory with :attr:`winner`. + + """ + + def __init__(self): + self.moves = [] + self.top = [0 for _ in range(7)] + self.winner = None + + @property + def last_player(self): + """ + Player who played the last move. + + """ + return PLAYER1 if len(self.moves) % 2 else PLAYER2 + + @property + def last_player_won(self): + """ + Whether the last move is winning. + + """ + b = sum(1 << (8 * column + row) for _, column, row in self.moves[::-2]) + return any(b & b >> v & b >> 2 * v & b >> 3 * v for v in [1, 7, 8, 9]) + + def play(self, player, column): + """ + Play a move in a column. + + Returns the row where the checker lands. + + Raises :exc:`RuntimeError` if the move is illegal. + + """ + if player == self.last_player: + raise RuntimeError("It isn't your turn.") + + row = self.top[column] + if row == 6: + raise RuntimeError("This slot is full.") + + self.moves.append((player, column, row)) + self.top[column] += 1 + + if self.winner is None and self.last_player_won: + self.winner = self.last_player + + return row diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/favicon.ico b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/favicon.ico Binary files differnew file mode 100644 index 00000000000..36e855029d7 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/favicon.ico diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/index.html b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/index.html new file mode 100644 index 00000000000..1a16f72a257 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/index.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>Connect Four</title> + </head> + <body> + <div class="actions"> + <a class="action new" href="/">New</a> + <a class="action join" href="">Join</a> + <a class="action watch" href="">Watch</a> + </div> + <div class="board"></div> + <script src="main.js" type="module"></script> + </body> +</html> diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/main.js b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/main.js new file mode 100644 index 00000000000..3000fa2f78c --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/main.js @@ -0,0 +1,93 @@ +import { createBoard, playMove } from "./connect4.js"; + +function getWebSocketServer() { + if (window.location.host === "python-websockets.github.io") { + return "wss://websockets-tutorial.herokuapp.com/"; + } else if (window.location.host === "localhost:8000") { + return "ws://localhost:8001/"; + } else { + throw new Error(`Unsupported host: ${window.location.host}`); + } +} + +function initGame(websocket) { + websocket.addEventListener("open", () => { + // Send an "init" event according to who is connecting. + const params = new URLSearchParams(window.location.search); + let event = { type: "init" }; + if (params.has("join")) { + // Second player joins an existing game. + event.join = params.get("join"); + } else if (params.has("watch")) { + // Spectator watches an existing game. + event.watch = params.get("watch"); + } else { + // First player starts a new game. + } + websocket.send(JSON.stringify(event)); + }); +} + +function showMessage(message) { + window.setTimeout(() => window.alert(message), 50); +} + +function receiveMoves(board, websocket) { + websocket.addEventListener("message", ({ data }) => { + const event = JSON.parse(data); + switch (event.type) { + case "init": + // Create links for inviting the second player and spectators. + document.querySelector(".join").href = "?join=" + event.join; + document.querySelector(".watch").href = "?watch=" + event.watch; + break; + case "play": + // Update the UI with the move. + playMove(board, event.player, event.column, event.row); + break; + case "win": + showMessage(`Player ${event.player} wins!`); + // No further messages are expected; close the WebSocket connection. + websocket.close(1000); + break; + case "error": + showMessage(event.message); + break; + default: + throw new Error(`Unsupported event type: ${event.type}.`); + } + }); +} + +function sendMoves(board, websocket) { + // Don't send moves for a spectator watching a game. + const params = new URLSearchParams(window.location.search); + if (params.has("watch")) { + return; + } + + // When clicking a column, send a "play" event for a move in that column. + board.addEventListener("click", ({ target }) => { + const column = target.dataset.column; + // Ignore clicks outside a column. + if (column === undefined) { + return; + } + const event = { + type: "play", + column: parseInt(column, 10), + }; + websocket.send(JSON.stringify(event)); + }); +} + +window.addEventListener("DOMContentLoaded", () => { + // Initialize the UI. + const board = document.querySelector(".board"); + createBoard(board); + // Open the WebSocket connection and register event handlers. + const websocket = new WebSocket(getWebSocketServer()); + initGame(websocket); + receiveMoves(board, websocket); + sendMoves(board, websocket); +}); diff --git a/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/requirements.txt b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/requirements.txt new file mode 100644 index 00000000000..14774b465e9 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/example/tutorial/step3/requirements.txt @@ -0,0 +1 @@ +websockets diff --git a/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/app.py b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/app.py new file mode 100644 index 00000000000..039e21174b1 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/app.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python + +import asyncio +import http +import http.cookies +import pathlib +import signal +import urllib.parse +import uuid + +import websockets +from websockets.frames import CloseCode + + +# User accounts database + +USERS = {} + + +def create_token(user, lifetime=1): + """Create token for user and delete it once its lifetime is over.""" + token = uuid.uuid4().hex + USERS[token] = user + asyncio.get_running_loop().call_later(lifetime, USERS.pop, token) + return token + + +def get_user(token): + """Find user authenticated by token or return None.""" + return USERS.get(token) + + +# Utilities + + +def get_cookie(raw, key): + cookie = http.cookies.SimpleCookie(raw) + morsel = cookie.get(key) + if morsel is not None: + return morsel.value + + +def get_query_param(path, key): + query = urllib.parse.urlparse(path).query + params = urllib.parse.parse_qs(query) + values = params.get(key, []) + if len(values) == 1: + return values[0] + + +# Main HTTP server + +CONTENT_TYPES = { + ".css": "text/css", + ".html": "text/html; charset=utf-8", + ".ico": "image/x-icon", + ".js": "text/javascript", +} + + +async def serve_html(path, request_headers): + user = get_query_param(path, "user") + path = urllib.parse.urlparse(path).path + if path == "/": + if user is None: + page = "index.html" + else: + page = "test.html" + else: + page = path[1:] + + try: + template = pathlib.Path(__file__).with_name(page) + except ValueError: + pass + else: + if template.is_file(): + headers = {"Content-Type": CONTENT_TYPES[template.suffix]} + body = template.read_bytes() + if user is not None: + token = create_token(user) + body = body.replace(b"TOKEN", token.encode()) + return http.HTTPStatus.OK, headers, body + + return http.HTTPStatus.NOT_FOUND, {}, b"Not found\n" + + +async def noop_handler(websocket): + pass + + +# Send credentials as the first message in the WebSocket connection + + +async def first_message_handler(websocket): + token = await websocket.recv() + user = get_user(token) + if user is None: + await websocket.close(CloseCode.INTERNAL_ERROR, "authentication failed") + return + + await websocket.send(f"Hello {user}!") + message = await websocket.recv() + assert message == f"Goodbye {user}." + + +# Add credentials to the WebSocket URI in a query parameter + + +class QueryParamProtocol(websockets.WebSocketServerProtocol): + async def process_request(self, path, headers): + token = get_query_param(path, "token") + if token is None: + return http.HTTPStatus.UNAUTHORIZED, [], b"Missing token\n" + + user = get_user(token) + if user is None: + return http.HTTPStatus.UNAUTHORIZED, [], b"Invalid token\n" + + self.user = user + + +async def query_param_handler(websocket): + user = websocket.user + + await websocket.send(f"Hello {user}!") + message = await websocket.recv() + assert message == f"Goodbye {user}." + + +# Set a cookie on the domain of the WebSocket URI + + +class CookieProtocol(websockets.WebSocketServerProtocol): + async def process_request(self, path, headers): + if "Upgrade" not in headers: + template = pathlib.Path(__file__).with_name(path[1:]) + headers = {"Content-Type": CONTENT_TYPES[template.suffix]} + body = template.read_bytes() + return http.HTTPStatus.OK, headers, body + + token = get_cookie(headers.get("Cookie", ""), "token") + if token is None: + return http.HTTPStatus.UNAUTHORIZED, [], b"Missing token\n" + + user = get_user(token) + if user is None: + return http.HTTPStatus.UNAUTHORIZED, [], b"Invalid token\n" + + self.user = user + + +async def cookie_handler(websocket): + user = websocket.user + + await websocket.send(f"Hello {user}!") + message = await websocket.recv() + assert message == f"Goodbye {user}." + + +# Adding credentials to the WebSocket URI in user information + + +class UserInfoProtocol(websockets.BasicAuthWebSocketServerProtocol): + async def check_credentials(self, username, password): + if username != "token": + return False + + user = get_user(password) + if user is None: + return False + + self.user = user + return True + + +async def user_info_handler(websocket): + user = websocket.user + + await websocket.send(f"Hello {user}!") + message = await websocket.recv() + assert message == f"Goodbye {user}." + + +# Start all five servers + + +async def main(): + # Set the stop condition when receiving SIGINT or SIGTERM. + loop = asyncio.get_running_loop() + stop = loop.create_future() + loop.add_signal_handler(signal.SIGINT, stop.set_result, None) + loop.add_signal_handler(signal.SIGTERM, stop.set_result, None) + + async with websockets.serve( + noop_handler, + host="", + port=8000, + process_request=serve_html, + ), websockets.serve( + first_message_handler, + host="", + port=8001, + ), websockets.serve( + query_param_handler, + host="", + port=8002, + create_protocol=QueryParamProtocol, + ), websockets.serve( + cookie_handler, + host="", + port=8003, + create_protocol=CookieProtocol, + ), websockets.serve( + user_info_handler, + host="", + port=8004, + create_protocol=UserInfoProtocol, + ): + print("Running on http://localhost:8000/") + await stop + print("\rExiting") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/cookie.html b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/cookie.html new file mode 100644 index 00000000000..ca17358fd0d --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/cookie.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>Cookie | WebSocket Authentication</title> + <link href="style.css" rel="stylesheet"> + </head> + <body class="test"> + <p class="test">[??] Cookie</p> + <p class="ok">[OK] Cookie</p> + <p class="ko">[KO] Cookie</p> + <script src="script.js"></script> + <script src="cookie.js"></script> + <iframe src="http://localhost:8003/cookie_iframe.html" style="display: none;"></iframe> + </body> +</html> diff --git a/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/cookie.js b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/cookie.js new file mode 100644 index 00000000000..2cca34fcbb4 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/cookie.js @@ -0,0 +1,23 @@ +// send token to iframe +window.addEventListener("DOMContentLoaded", () => { + const iframe = document.querySelector("iframe"); + iframe.addEventListener("load", () => { + iframe.contentWindow.postMessage(token, "http://localhost:8003"); + }); +}); + +// once iframe has set cookie, open WebSocket connection +window.addEventListener("message", ({ origin }) => { + if (origin !== "http://localhost:8003") { + return; + } + + const websocket = new WebSocket("ws://localhost:8003/"); + + websocket.onmessage = ({ data }) => { + // event.data is expected to be "Hello <user>!" + websocket.send(`Goodbye ${data.slice(6, -1)}.`); + }; + + runTest(websocket); +}); diff --git a/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/cookie_iframe.html b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/cookie_iframe.html new file mode 100644 index 00000000000..9f49ebb9a08 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/cookie_iframe.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>Cookie iframe | WebSocket Authentication</title> + </head> + <body> + <script src="cookie_iframe.js"></script> + </body> +</html> diff --git a/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/cookie_iframe.js b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/cookie_iframe.js new file mode 100644 index 00000000000..2d2e692e8d2 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/cookie_iframe.js @@ -0,0 +1,9 @@ +// receive token from the parent window, set cookie and notify parent +window.addEventListener("message", ({ origin, data }) => { + if (origin !== "http://localhost:8000") { + return; + } + + document.cookie = `token=${data}; SameSite=Strict`; + window.parent.postMessage("", "http://localhost:8000"); +}); diff --git a/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/favicon.ico b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/favicon.ico Binary files differnew file mode 100644 index 00000000000..36e855029d7 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/favicon.ico diff --git a/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/first_message.html b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/first_message.html new file mode 100644 index 00000000000..4dc511a1764 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/first_message.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>First message | WebSocket Authentication</title> + <link href="style.css" rel="stylesheet"> + </head> + <body class="test"> + <p class="test">[??] First message</p> + <p class="ok">[OK] First message</p> + <p class="ko">[KO] First message</p> + <script src="script.js"></script> + <script src="first_message.js"></script> + </body> +</html> diff --git a/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/first_message.js b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/first_message.js new file mode 100644 index 00000000000..1acf048bafb --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/first_message.js @@ -0,0 +1,11 @@ +window.addEventListener("DOMContentLoaded", () => { + const websocket = new WebSocket("ws://localhost:8001/"); + websocket.onopen = () => websocket.send(token); + + websocket.onmessage = ({ data }) => { + // event.data is expected to be "Hello <user>!" + websocket.send(`Goodbye ${data.slice(6, -1)}.`); + }; + + runTest(websocket); +}); diff --git a/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/index.html b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/index.html new file mode 100644 index 00000000000..c37deef2707 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/index.html @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>WebSocket Authentication</title> + <link href="style.css" rel="stylesheet"> + </head> + <body> + <form method="GET"> + <input name="user" placeholder="username"> + </form> + </body> +</html> diff --git a/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/query_param.html b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/query_param.html new file mode 100644 index 00000000000..27aa454a401 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/query_param.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>Query parameter | WebSocket Authentication</title> + <link href="style.css" rel="stylesheet"> + </head> + <body class="test"> + <p class="test">[??] Query parameter</p> + <p class="ok">[OK] Query parameter</p> + <p class="ko">[KO] Query parameter</p> + <script src="script.js"></script> + <script src="query_param.js"></script> + </body> +</html> diff --git a/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/query_param.js b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/query_param.js new file mode 100644 index 00000000000..6a54d0b6cae --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/query_param.js @@ -0,0 +1,11 @@ +window.addEventListener("DOMContentLoaded", () => { + const uri = `ws://localhost:8002/?token=${token}`; + const websocket = new WebSocket(uri); + + websocket.onmessage = ({ data }) => { + // event.data is expected to be "Hello <user>!" + websocket.send(`Goodbye ${data.slice(6, -1)}.`); + }; + + runTest(websocket); +}); diff --git a/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/script.js b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/script.js new file mode 100644 index 00000000000..ec4e5e6709b --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/script.js @@ -0,0 +1,51 @@ +var token = window.parent.token; + +function getExpectedEvents() { + return [ + { + type: "open", + }, + { + type: "message", + data: `Hello ${window.parent.user}!`, + }, + { + type: "close", + code: 1000, + reason: "", + wasClean: true, + }, + ]; +} + +function isEqual(expected, actual) { + // good enough for our purposes here! + return JSON.stringify(expected) === JSON.stringify(actual); +} + +function testStep(expected, actual) { + if (isEqual(expected, actual)) { + document.body.className = "ok"; + } else if (isEqual(expected.slice(0, actual.length), actual)) { + document.body.className = "test"; + } else { + document.body.className = "ko"; + } +} + +function runTest(websocket) { + const expected = getExpectedEvents(); + var actual = []; + websocket.addEventListener("open", ({ type }) => { + actual.push({ type }); + testStep(expected, actual); + }); + websocket.addEventListener("message", ({ type, data }) => { + actual.push({ type, data }); + testStep(expected, actual); + }); + websocket.addEventListener("close", ({ type, code, reason, wasClean }) => { + actual.push({ type, code, reason, wasClean }); + testStep(expected, actual); + }); +} diff --git a/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/style.css b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/style.css new file mode 100644 index 00000000000..6e3918ccae6 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/style.css @@ -0,0 +1,69 @@ +/* page layout */ + +body { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin: 0; + height: 100vh; +} +div.title, iframe { + width: 100vw; + height: 20vh; + border: none; +} +div.title { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} +h1, p { + margin: 0; + width: 24em; +} + +/* text style */ + +h1, input, p { + font-family: monospace; + font-size: 3em; +} +input { + color: #333; + border: 3px solid #999; + padding: 1em; +} +input:focus { + border-color: #333; + outline: none; +} +input::placeholder { + color: #999; + opacity: 1; +} + +/* test results */ + +body.test { + background-color: #666; + color: #fff; +} +body.ok { + background-color: #090; + color: #fff; +} +body.ko { + background-color: #900; + color: #fff; +} +body > p { + display: none; +} +body > p.title, +body.test > p.test, +body.ok > p.ok, +body.ko > p.ko { + display: block; +} diff --git a/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/test.html b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/test.html new file mode 100644 index 00000000000..3883d6a39e6 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/test.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>WebSocket Authentication</title> + <link href="style.css" rel="stylesheet"> + </head> + <body data-token="TOKEN"> + <div class="title"><h1>WebSocket Authentication</h1></div> + <iframe src="first_message.html"></iframe> + <iframe src="query_param.html"></iframe> + <iframe src="cookie.html"></iframe> + <iframe src="user_info.html"></iframe> + <script src="test.js"></script> + </body> +</html> diff --git a/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/test.js b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/test.js new file mode 100644 index 00000000000..428830ff31d --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/test.js @@ -0,0 +1,6 @@ +// for connecting to WebSocket servers +var token = document.body.dataset.token; + +// for test assertions only +const params = new URLSearchParams(window.location.search); +var user = params.get("user"); diff --git a/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/user_info.html b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/user_info.html new file mode 100644 index 00000000000..7b9c99c7301 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/user_info.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>User information | WebSocket Authentication</title> + <link href="style.css" rel="stylesheet"> + </head> + <body class="test"> + <p class="test">[??] User information</p> + <p class="ok">[OK] User information</p> + <p class="ko">[KO] User information</p> + <script src="script.js"></script> + <script src="user_info.js"></script> + </body> +</html> diff --git a/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/user_info.js b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/user_info.js new file mode 100644 index 00000000000..1dab2ce4c17 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/experiments/authentication/user_info.js @@ -0,0 +1,11 @@ +window.addEventListener("DOMContentLoaded", () => { + const uri = `ws://token:${token}@localhost:8004/`; + const websocket = new WebSocket(uri); + + websocket.onmessage = ({ data }) => { + // event.data is expected to be "Hello <user>!" + websocket.send(`Goodbye ${data.slice(6, -1)}.`); + }; + + runTest(websocket); +}); diff --git a/tests/wpt/tests/tools/third_party/websockets/experiments/broadcast/clients.py b/tests/wpt/tests/tools/third_party/websockets/experiments/broadcast/clients.py new file mode 100644 index 00000000000..fe39dfe051e --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/experiments/broadcast/clients.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python + +import asyncio +import statistics +import sys +import time + +import websockets + + +LATENCIES = {} + + +async def log_latency(interval): + while True: + await asyncio.sleep(interval) + p = statistics.quantiles(LATENCIES.values(), n=100) + print(f"clients = {len(LATENCIES)}") + print( + f"p50 = {p[49] / 1e6:.1f}ms, " + f"p95 = {p[94] / 1e6:.1f}ms, " + f"p99 = {p[98] / 1e6:.1f}ms" + ) + print() + + +async def client(): + try: + async with websockets.connect( + "ws://localhost:8765", + ping_timeout=None, + ) as websocket: + async for msg in websocket: + client_time = time.time_ns() + server_time = int(msg[:19].decode()) + LATENCIES[websocket] = client_time - server_time + except Exception as exc: + print(exc) + + +async def main(count, interval): + asyncio.create_task(log_latency(interval)) + clients = [] + for _ in range(count): + clients.append(asyncio.create_task(client())) + await asyncio.sleep(0.001) # 1ms between each connection + await asyncio.wait(clients) + + +if __name__ == "__main__": + try: + count = int(sys.argv[1]) + interval = float(sys.argv[2]) + except Exception as exc: + print(f"Usage: {sys.argv[0]} count interval") + print(" Connect <count> clients e.g. 1000") + print(" Report latency every <interval> seconds e.g. 1") + print() + print(exc) + else: + asyncio.run(main(count, interval)) diff --git a/tests/wpt/tests/tools/third_party/websockets/experiments/broadcast/server.py b/tests/wpt/tests/tools/third_party/websockets/experiments/broadcast/server.py new file mode 100644 index 00000000000..9c9907b7f9e --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/experiments/broadcast/server.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python + +import asyncio +import functools +import os +import sys +import time + +import websockets + + +CLIENTS = set() + + +async def send(websocket, message): + try: + await websocket.send(message) + except websockets.ConnectionClosed: + pass + + +async def relay(queue, websocket): + while True: + message = await queue.get() + await websocket.send(message) + + +class PubSub: + def __init__(self): + self.waiter = asyncio.Future() + + def publish(self, value): + waiter, self.waiter = self.waiter, asyncio.Future() + waiter.set_result((value, self.waiter)) + + async def subscribe(self): + waiter = self.waiter + while True: + value, waiter = await waiter + yield value + + __aiter__ = subscribe + + +PUBSUB = PubSub() + + +async def handler(websocket, method=None): + if method in ["default", "naive", "task", "wait"]: + CLIENTS.add(websocket) + try: + await websocket.wait_closed() + finally: + CLIENTS.remove(websocket) + elif method == "queue": + queue = asyncio.Queue() + relay_task = asyncio.create_task(relay(queue, websocket)) + CLIENTS.add(queue) + try: + await websocket.wait_closed() + finally: + CLIENTS.remove(queue) + relay_task.cancel() + elif method == "pubsub": + async for message in PUBSUB: + await websocket.send(message) + else: + raise NotImplementedError(f"unsupported method: {method}") + + +async def broadcast(method, size, delay): + """Broadcast messages at regular intervals.""" + load_average = 0 + time_average = 0 + pc1, pt1 = time.perf_counter_ns(), time.process_time_ns() + await asyncio.sleep(delay) + while True: + print(f"clients = {len(CLIENTS)}") + pc0, pt0 = time.perf_counter_ns(), time.process_time_ns() + load_average = 0.9 * load_average + 0.1 * (pt0 - pt1) / (pc0 - pc1) + print( + f"load = {(pt0 - pt1) / (pc0 - pc1) * 100:.1f}% / " + f"average = {load_average * 100:.1f}%, " + f"late = {(pc0 - pc1 - delay * 1e9) / 1e6:.1f} ms" + ) + pc1, pt1 = pc0, pt0 + + assert size > 20 + message = str(time.time_ns()).encode() + b" " + os.urandom(size - 20) + + if method == "default": + websockets.broadcast(CLIENTS, message) + elif method == "naive": + # Since the loop can yield control, make a copy of CLIENTS + # to avoid: RuntimeError: Set changed size during iteration + for websocket in CLIENTS.copy(): + await send(websocket, message) + elif method == "task": + for websocket in CLIENTS: + asyncio.create_task(send(websocket, message)) + elif method == "wait": + if CLIENTS: # asyncio.wait doesn't accept an empty list + await asyncio.wait( + [ + asyncio.create_task(send(websocket, message)) + for websocket in CLIENTS + ] + ) + elif method == "queue": + for queue in CLIENTS: + queue.put_nowait(message) + elif method == "pubsub": + PUBSUB.publish(message) + else: + raise NotImplementedError(f"unsupported method: {method}") + + pc2 = time.perf_counter_ns() + wait = delay + (pc1 - pc2) / 1e9 + time_average = 0.9 * time_average + 0.1 * (pc2 - pc1) + print( + f"broadcast = {(pc2 - pc1) / 1e6:.1f}ms / " + f"average = {time_average / 1e6:.1f}ms, " + f"wait = {wait * 1e3:.1f}ms" + ) + await asyncio.sleep(wait) + print() + + +async def main(method, size, delay): + async with websockets.serve( + functools.partial(handler, method=method), + "localhost", + 8765, + compression=None, + ping_timeout=None, + ): + await broadcast(method, size, delay) + + +if __name__ == "__main__": + try: + method = sys.argv[1] + assert method in ["default", "naive", "task", "wait", "queue", "pubsub"] + size = int(sys.argv[2]) + delay = float(sys.argv[3]) + except Exception as exc: + print(f"Usage: {sys.argv[0]} method size delay") + print(" Start a server broadcasting messages with <method> e.g. naive") + print(" Send a payload of <size> bytes every <delay> seconds") + print() + print(exc) + else: + asyncio.run(main(method, size, delay)) diff --git a/tests/wpt/tests/tools/third_party/websockets/experiments/compression/benchmark.py b/tests/wpt/tests/tools/third_party/websockets/experiments/compression/benchmark.py new file mode 100644 index 00000000000..c5b13c8fa34 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/experiments/compression/benchmark.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python + +import getpass +import json +import pickle +import subprocess +import sys +import time +import zlib + + +CORPUS_FILE = "corpus.pkl" + +REPEAT = 10 + +WB, ML = 12, 5 # defaults used as a reference + + +def _corpus(): + OAUTH_TOKEN = getpass.getpass("OAuth Token? ") + COMMIT_API = ( + f'curl -H "Authorization: token {OAUTH_TOKEN}" ' + f"https://api.github.com/repos/python-websockets/websockets/git/commits/:sha" + ) + + commits = [] + + head = subprocess.check_output("git rev-parse HEAD", shell=True).decode().strip() + todo = [head] + seen = set() + + while todo: + sha = todo.pop(0) + commit = subprocess.check_output(COMMIT_API.replace(":sha", sha), shell=True) + commits.append(commit) + seen.add(sha) + for parent in json.loads(commit)["parents"]: + sha = parent["sha"] + if sha not in seen and sha not in todo: + todo.append(sha) + time.sleep(1) # rate throttling + + return commits + + +def corpus(): + data = _corpus() + with open(CORPUS_FILE, "wb") as handle: + pickle.dump(data, handle) + + +def _run(data): + size = {} + duration = {} + + for wbits in range(9, 16): + size[wbits] = {} + duration[wbits] = {} + + for memLevel in range(1, 10): + encoder = zlib.compressobj(wbits=-wbits, memLevel=memLevel) + encoded = [] + + t0 = time.perf_counter() + + for _ in range(REPEAT): + for item in data: + if isinstance(item, str): + item = item.encode("utf-8") + # Taken from PerMessageDeflate.encode + item = encoder.compress(item) + encoder.flush(zlib.Z_SYNC_FLUSH) + if item.endswith(b"\x00\x00\xff\xff"): + item = item[:-4] + encoded.append(item) + + t1 = time.perf_counter() + + size[wbits][memLevel] = sum(len(item) for item in encoded) + duration[wbits][memLevel] = (t1 - t0) / REPEAT + + raw_size = sum(len(item) for item in data) + + print("=" * 79) + print("Compression ratio") + print("=" * 79) + print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in range(1, 10)])) + for wbits in range(9, 16): + print( + "\t".join( + [str(wbits)] + + [ + f"{100 * (1 - size[wbits][memLevel] / raw_size):.1f}%" + for memLevel in range(1, 10) + ] + ) + ) + print("=" * 79) + print() + + print("=" * 79) + print("CPU time") + print("=" * 79) + print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in range(1, 10)])) + for wbits in range(9, 16): + print( + "\t".join( + [str(wbits)] + + [ + f"{1000 * duration[wbits][memLevel]:.1f}ms" + for memLevel in range(1, 10) + ] + ) + ) + print("=" * 79) + print() + + print("=" * 79) + print(f"Size vs. {WB} \\ {ML}") + print("=" * 79) + print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in range(1, 10)])) + for wbits in range(9, 16): + print( + "\t".join( + [str(wbits)] + + [ + f"{100 * (size[wbits][memLevel] / size[WB][ML] - 1):.1f}%" + for memLevel in range(1, 10) + ] + ) + ) + print("=" * 79) + print() + + print("=" * 79) + print(f"Time vs. {WB} \\ {ML}") + print("=" * 79) + print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in range(1, 10)])) + for wbits in range(9, 16): + print( + "\t".join( + [str(wbits)] + + [ + f"{100 * (duration[wbits][memLevel] / duration[WB][ML] - 1):.1f}%" + for memLevel in range(1, 10) + ] + ) + ) + print("=" * 79) + print() + + +def run(): + with open(CORPUS_FILE, "rb") as handle: + data = pickle.load(handle) + _run(data) + + +try: + run = globals()[sys.argv[1]] +except (KeyError, IndexError): + print(f"Usage: {sys.argv[0]} [corpus|run]") +else: + run() diff --git a/tests/wpt/tests/tools/third_party/websockets/experiments/compression/client.py b/tests/wpt/tests/tools/third_party/websockets/experiments/compression/client.py new file mode 100644 index 00000000000..3ee19ddc59a --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/experiments/compression/client.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python + +import asyncio +import statistics +import tracemalloc + +import websockets +from websockets.extensions import permessage_deflate + + +CLIENTS = 20 +INTERVAL = 1 / 10 # seconds + +WB, ML = 12, 5 + +MEM_SIZE = [] + + +async def client(client): + # Space out connections to make them sequential. + await asyncio.sleep(client * INTERVAL) + + tracemalloc.start() + + async with websockets.connect( + "ws://localhost:8765", + extensions=[ + permessage_deflate.ClientPerMessageDeflateFactory( + server_max_window_bits=WB, + client_max_window_bits=WB, + compress_settings={"memLevel": ML}, + ) + ], + ) as ws: + await ws.send("hello") + await ws.recv() + + await ws.send(b"hello") + await ws.recv() + + MEM_SIZE.append(tracemalloc.get_traced_memory()[0]) + tracemalloc.stop() + + # Hold connection open until the end of the test. + await asyncio.sleep(CLIENTS * INTERVAL) + + +async def clients(): + await asyncio.gather(*[client(client) for client in range(CLIENTS + 1)]) + + +asyncio.run(clients()) + + +# First connection incurs non-representative setup costs. +del MEM_SIZE[0] + +print(f"µ = {statistics.mean(MEM_SIZE) / 1024:.1f} KiB") +print(f"σ = {statistics.stdev(MEM_SIZE) / 1024:.1f} KiB") diff --git a/tests/wpt/tests/tools/third_party/websockets/experiments/compression/server.py b/tests/wpt/tests/tools/third_party/websockets/experiments/compression/server.py new file mode 100644 index 00000000000..8d1ee3cd7c0 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/experiments/compression/server.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python + +import asyncio +import os +import signal +import statistics +import tracemalloc + +import websockets +from websockets.extensions import permessage_deflate + + +CLIENTS = 20 +INTERVAL = 1 / 10 # seconds + +WB, ML = 12, 5 + +MEM_SIZE = [] + + +async def handler(ws): + msg = await ws.recv() + await ws.send(msg) + + msg = await ws.recv() + await ws.send(msg) + + MEM_SIZE.append(tracemalloc.get_traced_memory()[0]) + tracemalloc.stop() + + tracemalloc.start() + + # Hold connection open until the end of the test. + await asyncio.sleep(CLIENTS * INTERVAL) + + +async def server(): + loop = asyncio.get_running_loop() + stop = loop.create_future() + + # Set the stop condition when receiving SIGTERM. + print("Stop the server with:") + print(f"kill -TERM {os.getpid()}") + print() + loop.add_signal_handler(signal.SIGTERM, stop.set_result, None) + + async with websockets.serve( + handler, + "localhost", + 8765, + extensions=[ + permessage_deflate.ServerPerMessageDeflateFactory( + server_max_window_bits=WB, + client_max_window_bits=WB, + compress_settings={"memLevel": ML}, + ) + ], + ): + tracemalloc.start() + await stop + + +asyncio.run(server()) + + +# First connection may incur non-representative setup costs. +del MEM_SIZE[0] + +print(f"µ = {statistics.mean(MEM_SIZE) / 1024:.1f} KiB") +print(f"σ = {statistics.stdev(MEM_SIZE) / 1024:.1f} KiB") diff --git a/tests/wpt/tests/tools/third_party/websockets/experiments/optimization/parse_frames.py b/tests/wpt/tests/tools/third_party/websockets/experiments/optimization/parse_frames.py new file mode 100644 index 00000000000..e3acbe3c201 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/experiments/optimization/parse_frames.py @@ -0,0 +1,101 @@ +"""Benchark parsing WebSocket frames.""" + +import subprocess +import sys +import timeit + +from websockets.extensions.permessage_deflate import PerMessageDeflate +from websockets.frames import Frame, Opcode +from websockets.streams import StreamReader + + +# 256kB of text, compressible by about 70%. +text = subprocess.check_output(["git", "log", "8dd8e410"], text=True) + + +def get_frame(size): + repeat, remainder = divmod(size, 256 * 1024) + payload = repeat * text + text[:remainder] + return Frame(Opcode.TEXT, payload.encode(), True) + + +def parse_frame(data, count, mask, extensions): + reader = StreamReader() + for _ in range(count): + reader.feed_data(data) + parser = Frame.parse( + reader.read_exact, + mask=mask, + extensions=extensions, + ) + try: + next(parser) + except StopIteration: + pass + else: + assert False, "parser should return frame" + reader.feed_eof() + assert reader.at_eof(), "parser should consume all data" + + +def run_benchmark(size, count, compression=False, number=100): + if compression: + extensions = [PerMessageDeflate(True, True, 12, 12, {"memLevel": 5})] + else: + extensions = [] + globals = { + "get_frame": get_frame, + "parse_frame": parse_frame, + "extensions": extensions, + } + sppf = ( + min( + timeit.repeat( + f"parse_frame(data, {count}, mask=True, extensions=extensions)", + f"data = get_frame({size})" + f".serialize(mask=True, extensions=extensions)", + number=number, + globals=globals, + ) + ) + / number + / count + * 1_000_000 + ) + cppf = ( + min( + timeit.repeat( + f"parse_frame(data, {count}, mask=False, extensions=extensions)", + f"data = get_frame({size})" + f".serialize(mask=False, extensions=extensions)", + number=number, + globals=globals, + ) + ) + / number + / count + * 1_000_000 + ) + print(f"{size}\t{compression}\t{sppf:.2f}\t{cppf:.2f}") + + +if __name__ == "__main__": + print("Sizes are in bytes. Times are in µs per frame.", file=sys.stderr) + print("Run `tabs -16` for clean output. Pipe stdout to TSV for saving.") + print(file=sys.stderr) + + print("size\tcompression\tserver\tclient") + run_benchmark(size=8, count=1000, compression=False) + run_benchmark(size=60, count=1000, compression=False) + run_benchmark(size=500, count=1000, compression=False) + run_benchmark(size=4_000, count=1000, compression=False) + run_benchmark(size=30_000, count=200, compression=False) + run_benchmark(size=250_000, count=100, compression=False) + run_benchmark(size=2_000_000, count=20, compression=False) + + run_benchmark(size=8, count=1000, compression=True) + run_benchmark(size=60, count=1000, compression=True) + run_benchmark(size=500, count=200, compression=True) + run_benchmark(size=4_000, count=100, compression=True) + run_benchmark(size=30_000, count=20, compression=True) + run_benchmark(size=250_000, count=10, compression=True) diff --git a/tests/wpt/tests/tools/third_party/websockets/experiments/optimization/parse_handshake.py b/tests/wpt/tests/tools/third_party/websockets/experiments/optimization/parse_handshake.py new file mode 100644 index 00000000000..af5a4ecae2c --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/experiments/optimization/parse_handshake.py @@ -0,0 +1,102 @@ +"""Benchark parsing WebSocket handshake requests.""" + +# The parser for responses is designed similarly and should perform similarly. + +import sys +import timeit + +from websockets.http11 import Request +from websockets.streams import StreamReader + + +CHROME_HANDSHAKE = ( + b"GET / HTTP/1.1\r\n" + b"Host: localhost:5678\r\n" + b"Connection: Upgrade\r\n" + b"Pragma: no-cache\r\n" + b"Cache-Control: no-cache\r\n" + b"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " + b"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36\r\n" + b"Upgrade: websocket\r\n" + b"Origin: null\r\n" + b"Sec-WebSocket-Version: 13\r\n" + b"Accept-Encoding: gzip, deflate, br\r\n" + b"Accept-Language: en-GB,en;q=0.9,en-US;q=0.8,fr;q=0.7\r\n" + b"Sec-WebSocket-Key: ebkySAl+8+e6l5pRKTMkyQ==\r\n" + b"Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n" + b"\r\n" +) + +FIREFOX_HANDSHAKE = ( + b"GET / HTTP/1.1\r\n" + b"Host: localhost:5678\r\n" + b"User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) " + b"Gecko/20100101 Firefox/111.0\r\n" + b"Accept: */*\r\n" + b"Accept-Language: en-US,en;q=0.7,fr-FR;q=0.3\r\n" + b"Accept-Encoding: gzip, deflate, br\r\n" + b"Sec-WebSocket-Version: 13\r\n" + b"Origin: null\r\n" + b"Sec-WebSocket-Extensions: permessage-deflate\r\n" + b"Sec-WebSocket-Key: 1PuS+hnb+0AXsL7z2hNAhw==\r\n" + b"Connection: keep-alive, Upgrade\r\n" + b"Sec-Fetch-Dest: websocket\r\n" + b"Sec-Fetch-Mode: websocket\r\n" + b"Sec-Fetch-Site: cross-site\r\n" + b"Pragma: no-cache\r\n" + b"Cache-Control: no-cache\r\n" + b"Upgrade: websocket\r\n" + b"\r\n" +) + +WEBSOCKETS_HANDSHAKE = ( + b"GET / HTTP/1.1\r\n" + b"Host: localhost:8765\r\n" + b"Upgrade: websocket\r\n" + b"Connection: Upgrade\r\n" + b"Sec-WebSocket-Key: 9c55e0/siQ6tJPCs/QR8ZA==\r\n" + b"Sec-WebSocket-Version: 13\r\n" + b"Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n" + b"User-Agent: Python/3.11 websockets/11.0\r\n" + b"\r\n" +) + + +def parse_handshake(handshake): + reader = StreamReader() + reader.feed_data(handshake) + parser = Request.parse(reader.read_line) + try: + next(parser) + except StopIteration: + pass + else: + assert False, "parser should return request" + reader.feed_eof() + assert reader.at_eof(), "parser should consume all data" + + +def run_benchmark(name, handshake, number=10000): + ph = ( + min( + timeit.repeat( + "parse_handshake(handshake)", + number=number, + globals={"parse_handshake": parse_handshake, "handshake": handshake}, + ) + ) + / number + * 1_000_000 + ) + print(f"{name}\t{len(handshake)}\t{ph:.1f}") + + +if __name__ == "__main__": + print("Sizes are in bytes. Times are in µs per frame.", file=sys.stderr) + print("Run `tabs -16` for clean output. Pipe stdout to TSV for saving.") + print(file=sys.stderr) + + print("client\tsize\ttime") + run_benchmark("Chrome", CHROME_HANDSHAKE) + run_benchmark("Firefox", FIREFOX_HANDSHAKE) + run_benchmark("websockets", WEBSOCKETS_HANDSHAKE) diff --git a/tests/wpt/tests/tools/third_party/websockets/experiments/optimization/streams.py b/tests/wpt/tests/tools/third_party/websockets/experiments/optimization/streams.py new file mode 100644 index 00000000000..ca24a598345 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/experiments/optimization/streams.py @@ -0,0 +1,301 @@ +""" +Benchmark two possible implementations of a stream reader. + +The difference lies in the data structure that buffers incoming data: + +* ``ByteArrayStreamReader`` uses a ``bytearray``; +* ``BytesDequeStreamReader`` uses a ``deque[bytes]``. + +``ByteArrayStreamReader`` is faster for streaming small frames, which is the +standard use case of websockets, likely due to its simple implementation and +to ``bytearray`` being fast at appending data and removing data at the front +(https://hg.python.org/cpython/rev/499a96611baa). + +``BytesDequeStreamReader`` is faster for large frames and for bursts, likely +because it copies payloads only once, while ``ByteArrayStreamReader`` copies +them twice. + +""" + + +import collections +import os +import timeit + + +# Implementations + + +class ByteArrayStreamReader: + def __init__(self): + self.buffer = bytearray() + self.eof = False + + def readline(self): + n = 0 # number of bytes to read + p = 0 # number of bytes without a newline + while True: + n = self.buffer.find(b"\n", p) + 1 + if n > 0: + break + p = len(self.buffer) + yield + r = self.buffer[:n] + del self.buffer[:n] + return r + + def readexactly(self, n): + assert n >= 0 + while len(self.buffer) < n: + yield + r = self.buffer[:n] + del self.buffer[:n] + return r + + def feed_data(self, data): + self.buffer += data + + def feed_eof(self): + self.eof = True + + def at_eof(self): + return self.eof and not self.buffer + + +class BytesDequeStreamReader: + def __init__(self): + self.buffer = collections.deque() + self.eof = False + + def readline(self): + b = [] + while True: + # Read next chunk + while True: + try: + c = self.buffer.popleft() + except IndexError: + yield + else: + break + # Handle chunk + n = c.find(b"\n") + 1 + if n == len(c): + # Read exactly enough data + b.append(c) + break + elif n > 0: + # Read too much data + b.append(c[:n]) + self.buffer.appendleft(c[n:]) + break + else: # n == 0 + # Need to read more data + b.append(c) + return b"".join(b) + + def readexactly(self, n): + if n == 0: + return b"" + b = [] + while True: + # Read next chunk + while True: + try: + c = self.buffer.popleft() + except IndexError: + yield + else: + break + # Handle chunk + n -= len(c) + if n == 0: + # Read exactly enough data + b.append(c) + break + elif n < 0: + # Read too much data + b.append(c[:n]) + self.buffer.appendleft(c[n:]) + break + else: # n >= 0 + # Need to read more data + b.append(c) + return b"".join(b) + + def feed_data(self, data): + self.buffer.append(data) + + def feed_eof(self): + self.eof = True + + def at_eof(self): + return self.eof and not self.buffer + + +# Tests + + +class Protocol: + def __init__(self, StreamReader): + self.reader = StreamReader() + self.events = [] + # Start parser coroutine + self.parser = self.run_parser() + next(self.parser) + + def run_parser(self): + while True: + frame = yield from self.reader.readexactly(2) + self.events.append(frame) + frame = yield from self.reader.readline() + self.events.append(frame) + + def data_received(self, data): + self.reader.feed_data(data) + next(self.parser) # run parser until more data is needed + events, self.events = self.events, [] + return events + + +def run_test(StreamReader): + proto = Protocol(StreamReader) + + actual = proto.data_received(b"a") + expected = [] + assert actual == expected, f"{actual} != {expected}" + + actual = proto.data_received(b"b") + expected = [b"ab"] + assert actual == expected, f"{actual} != {expected}" + + actual = proto.data_received(b"c") + expected = [] + assert actual == expected, f"{actual} != {expected}" + + actual = proto.data_received(b"\n") + expected = [b"c\n"] + assert actual == expected, f"{actual} != {expected}" + + actual = proto.data_received(b"efghi\njklmn") + expected = [b"ef", b"ghi\n", b"jk"] + assert actual == expected, f"{actual} != {expected}" + + +# Benchmarks + + +def get_frame_packets(size, packet_size=None): + if size < 126: + frame = bytes([138, size]) + elif size < 65536: + frame = bytes([138, 126]) + bytes(divmod(size, 256)) + else: + size1, size2 = divmod(size, 65536) + frame = ( + bytes([138, 127]) + bytes(divmod(size1, 256)) + bytes(divmod(size2, 256)) + ) + frame += os.urandom(size) + if packet_size is None: + return [frame] + else: + packets = [] + while frame: + packets.append(frame[:packet_size]) + frame = frame[packet_size:] + return packets + + +def benchmark_stream(StreamReader, packets, size, count): + reader = StreamReader() + for _ in range(count): + for packet in packets: + reader.feed_data(packet) + yield from reader.readexactly(2) + if size >= 65536: + yield from reader.readexactly(4) + elif size >= 126: + yield from reader.readexactly(2) + yield from reader.readexactly(size) + reader.feed_eof() + assert reader.at_eof() + + +def benchmark_burst(StreamReader, packets, size, count): + reader = StreamReader() + for _ in range(count): + for packet in packets: + reader.feed_data(packet) + reader.feed_eof() + for _ in range(count): + yield from reader.readexactly(2) + if size >= 65536: + yield from reader.readexactly(4) + elif size >= 126: + yield from reader.readexactly(2) + yield from reader.readexactly(size) + assert reader.at_eof() + + +def run_benchmark(size, count, packet_size=None, number=1000): + stmt = f"list(benchmark(StreamReader, packets, {size}, {count}))" + setup = f"packets = get_frame_packets({size}, {packet_size})" + context = globals() + + context["StreamReader"] = context["ByteArrayStreamReader"] + context["benchmark"] = context["benchmark_stream"] + bas = min(timeit.repeat(stmt, setup, number=number, globals=context)) + context["benchmark"] = context["benchmark_burst"] + bab = min(timeit.repeat(stmt, setup, number=number, globals=context)) + + context["StreamReader"] = context["BytesDequeStreamReader"] + context["benchmark"] = context["benchmark_stream"] + bds = min(timeit.repeat(stmt, setup, number=number, globals=context)) + context["benchmark"] = context["benchmark_burst"] + bdb = min(timeit.repeat(stmt, setup, number=number, globals=context)) + + print( + f"Frame size = {size} bytes, " + f"frame count = {count}, " + f"packet size = {packet_size}" + ) + print(f"* ByteArrayStreamReader (stream): {bas / number * 1_000_000:.1f}µs") + print( + f"* BytesDequeStreamReader (stream): " + f"{bds / number * 1_000_000:.1f}µs ({(bds / bas - 1) * 100:+.1f}%)" + ) + print(f"* ByteArrayStreamReader (burst): {bab / number * 1_000_000:.1f}µs") + print( + f"* BytesDequeStreamReader (burst): " + f"{bdb / number * 1_000_000:.1f}µs ({(bdb / bab - 1) * 100:+.1f}%)" + ) + print() + + +if __name__ == "__main__": + run_test(ByteArrayStreamReader) + run_test(BytesDequeStreamReader) + + run_benchmark(size=8, count=1000) + run_benchmark(size=60, count=1000) + run_benchmark(size=500, count=500) + run_benchmark(size=4_000, count=200) + run_benchmark(size=30_000, count=100) + run_benchmark(size=250_000, count=50) + run_benchmark(size=2_000_000, count=20) + + run_benchmark(size=4_000, count=200, packet_size=1024) + run_benchmark(size=30_000, count=100, packet_size=1024) + run_benchmark(size=250_000, count=50, packet_size=1024) + run_benchmark(size=2_000_000, count=20, packet_size=1024) + + run_benchmark(size=30_000, count=100, packet_size=4096) + run_benchmark(size=250_000, count=50, packet_size=4096) + run_benchmark(size=2_000_000, count=20, packet_size=4096) + + run_benchmark(size=30_000, count=100, packet_size=16384) + run_benchmark(size=250_000, count=50, packet_size=16384) + run_benchmark(size=2_000_000, count=20, packet_size=16384) + + run_benchmark(size=250_000, count=50, packet_size=65536) + run_benchmark(size=2_000_000, count=20, packet_size=65536) diff --git a/tests/wpt/tests/tools/third_party/websockets/fuzzing/fuzz_http11_request_parser.py b/tests/wpt/tests/tools/third_party/websockets/fuzzing/fuzz_http11_request_parser.py new file mode 100644 index 00000000000..59e0cea0f43 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/fuzzing/fuzz_http11_request_parser.py @@ -0,0 +1,42 @@ +import sys + +import atheris + + +with atheris.instrument_imports(): + from websockets.exceptions import SecurityError + from websockets.http11 import Request + from websockets.streams import StreamReader + + +def test_one_input(data): + reader = StreamReader() + reader.feed_data(data) + reader.feed_eof() + + parser = Request.parse( + reader.read_line, + ) + + try: + next(parser) + except StopIteration as exc: + assert isinstance(exc.value, Request) + return # input accepted + except ( + EOFError, # connection is closed without a full HTTP request + SecurityError, # request exceeds a security limit + ValueError, # request isn't well formatted + ): + return # input rejected with a documented exception + + raise RuntimeError("parsing didn't complete") + + +def main(): + atheris.Setup(sys.argv, test_one_input) + atheris.Fuzz() + + +if __name__ == "__main__": + main() diff --git a/tests/wpt/tests/tools/third_party/websockets/fuzzing/fuzz_http11_response_parser.py b/tests/wpt/tests/tools/third_party/websockets/fuzzing/fuzz_http11_response_parser.py new file mode 100644 index 00000000000..6906720a49d --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/fuzzing/fuzz_http11_response_parser.py @@ -0,0 +1,44 @@ +import sys + +import atheris + + +with atheris.instrument_imports(): + from websockets.exceptions import SecurityError + from websockets.http11 import Response + from websockets.streams import StreamReader + + +def test_one_input(data): + reader = StreamReader() + reader.feed_data(data) + reader.feed_eof() + + parser = Response.parse( + reader.read_line, + reader.read_exact, + reader.read_to_eof, + ) + try: + next(parser) + except StopIteration as exc: + assert isinstance(exc.value, Response) + return # input accepted + except ( + EOFError, # connection is closed without a full HTTP response + SecurityError, # response exceeds a security limit + LookupError, # response isn't well formatted + ValueError, # response isn't well formatted + ): + return # input rejected with a documented exception + + raise RuntimeError("parsing didn't complete") + + +def main(): + atheris.Setup(sys.argv, test_one_input) + atheris.Fuzz() + + +if __name__ == "__main__": + main() diff --git a/tests/wpt/tests/tools/third_party/websockets/fuzzing/fuzz_websocket_parser.py b/tests/wpt/tests/tools/third_party/websockets/fuzzing/fuzz_websocket_parser.py new file mode 100644 index 00000000000..1509a3549d7 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/fuzzing/fuzz_websocket_parser.py @@ -0,0 +1,51 @@ +import sys + +import atheris + + +with atheris.instrument_imports(): + from websockets.exceptions import PayloadTooBig, ProtocolError + from websockets.frames import Frame + from websockets.streams import StreamReader + + +def test_one_input(data): + fdp = atheris.FuzzedDataProvider(data) + mask = fdp.ConsumeBool() + max_size_enabled = fdp.ConsumeBool() + max_size = fdp.ConsumeInt(4) + payload = fdp.ConsumeBytes(atheris.ALL_REMAINING) + + reader = StreamReader() + reader.feed_data(payload) + reader.feed_eof() + + parser = Frame.parse( + reader.read_exact, + mask=mask, + max_size=max_size if max_size_enabled else None, + ) + + try: + next(parser) + except StopIteration as exc: + assert isinstance(exc.value, Frame) + return # input accepted + except ( + EOFError, # connection is closed without a full WebSocket frame + UnicodeDecodeError, # frame contains invalid UTF-8 + PayloadTooBig, # frame's payload size exceeds ``max_size`` + ProtocolError, # frame contains incorrect values + ): + return # input rejected with a documented exception + + raise RuntimeError("parsing didn't complete") + + +def main(): + atheris.Setup(sys.argv, test_one_input) + atheris.Fuzz() + + +if __name__ == "__main__": + main() diff --git a/tests/wpt/tests/tools/third_party/websockets/logo/favicon.ico b/tests/wpt/tests/tools/third_party/websockets/logo/favicon.ico Binary files differnew file mode 100644 index 00000000000..36e855029d7 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/logo/favicon.ico diff --git a/tests/wpt/tests/tools/third_party/websockets/logo/github-social-preview.html b/tests/wpt/tests/tools/third_party/websockets/logo/github-social-preview.html new file mode 100644 index 00000000000..7f2b45badb4 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/logo/github-social-preview.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>GitHub social preview</title> + <style> + body { + background-color: black; + color: white; + font-family: -apple-system; + font-size: 36px; + font-weight: 100; + text-align: center; + } + p.screenshot { + background-color: white; + box-sizing: border-box; + width: 1280px; + height: 640px; + margin: 40px auto; + padding: 40px; + } + p.screenshot.x2 { + width: 640px; + height: 320px; + padding: 20px; + } + p.screenshot img { + height: 100%; + } + </style> + </head> + <body> + <p>Take a screenshot of this DOM node to make a PNG.</p> + <p>For 2x DPI screens.</p> + <p class="screenshot x2"><img src="vertical.svg" alt="preview @ 2x"></p> + <p>For regular screens.</p> + <p class="screenshot"><img src="vertical.svg" alt="preview"></p> + </body> +</html> diff --git a/tests/wpt/tests/tools/third_party/websockets/logo/github-social-preview.png b/tests/wpt/tests/tools/third_party/websockets/logo/github-social-preview.png Binary files differnew file mode 100644 index 00000000000..59a51b6e338 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/logo/github-social-preview.png diff --git a/tests/wpt/tests/tools/third_party/websockets/logo/horizontal.svg b/tests/wpt/tests/tools/third_party/websockets/logo/horizontal.svg new file mode 100644 index 00000000000..ee872dc4786 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/logo/horizontal.svg @@ -0,0 +1,31 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="256" viewBox="0 0 1024 256"> + <linearGradient id="w" x1="0" y1="0" x2="0.1667" y2="0.6667"> + <stop offset="0%" stop-color="#ffe873" /> + <stop offset="100%" stop-color="#ffd43b" /> + </linearGradient> + <linearGradient id="s" x1="0" y1="0" x2="0.1667" y2="0.6667"> + <stop offset="0%" stop-color="#5a9fd4" /> + <stop offset="100%" stop-color="#306998" /> + </linearGradient> +<g> + <path fill="url(#w)" d="m 151.60708,154.81618 c -0.43704,0.0747 -0.88656,0.12978 -1.35572,0.14933 -2.45813,0.0764 -4.25357,-0.58665 -5.82335,-2.15107 l -8.89246,-8.85942 -11.23464,-11.19805 -36.040757,-35.919452 c -3.43568,-3.42217 -7.332485,-5.347474 -11.589626,-5.723468 -2.229803,-0.198219 -4.473877,0.03111 -6.640354,0.675545 -3.242133,0.944875 -6.135526,2.664848 -8.593662,5.116366 -3.834369,3.819499 -5.86349,8.414979 -5.875977,13.287799 -0.06065,4.95281 1.951523,9.60074 5.808192,13.44424 l 55.622894,55.43648 c 1.82219,1.84175 2.65971,3.79549 2.63384,6.14568 l 0.004,0.208 c 0.0527,2.43196 -0.75991,4.34571 -2.6267,6.20612 -1.78028,1.77598 -3.8094,2.65241 -6.30945,2.75552 -2.45814,0.0764 -4.25446,-0.58844 -5.82514,-2.15286 L 48.702551,136.2618 c -5.214172,-5.19459 -11.702899,-6.98745 -18.22998,-5.04881 -3.245701,0.9431 -6.135527,2.66307 -8.595446,5.11459 -3.83437,3.82127 -5.865275,8.41676 -5.875978,13.28957 -0.05619,4.95281 1.951524,9.60252 5.806409,13.4478 l 58.10689,57.90577 c 8.319842,8.29143 19.340421,11.9376 32.743314,10.83806 12.57967,-1.02043 23.02317,-5.5848 31.03441,-13.57313 7.51265,-7.4861 11.96423,-16.35175 13.28695,-26.42537 10.47206,-1.68264 19.29494,-6.04524 26.27512,-13.00158 4.01364,-3.99994 7.14963,-8.3972 9.40531,-13.16157 -14.15569,-0.39911 -28.23645,-4.00972 -41.05247,-10.83095 z" /> + <path fill="url(#s)" d="m 196.96038,146.11854 c 0.10259,-12.84514 -4.43017,-23.98541 -13.50635,-33.1346 L 147.57292,77.225374 c -0.24349,-0.240885 -0.46469,-0.487992 -0.68678,-0.744877 -1.48416,-1.739529 -2.18788,-3.583056 -2.21018,-5.807022 -0.0259,-2.470184 0.84911,-4.508375 2.7605,-6.407902 1.91406,-1.909304 3.8531,-2.737735 6.36564,-2.684403 2.53662,0.024 4.62728,0.943097 6.57257,2.881734 l 60.59178,60.384846 12.11408,-12.06914 c 1.12203,-0.90755 1.95777,-1.76887 2.87823,-2.93418 5.91879,-7.51544 5.26947,-18.272609 -1.51003,-25.02895 L 187.20456,37.727314 c -9.19393,-9.157192 -20.36703,-13.776677 -33.16789,-13.7269 -12.94266,-0.05067 -24.14163,4.548375 -33.28739,13.662901 -9.02892,8.996307 -13.64015,19.93925 -13.7008,32.487501 l -0.004,0.14222 c -0.002,0.167998 -0.005,0.336884 -0.005,0.506659 -0.091,12.232701 4.10729,22.95787 12.48154,31.881285 0.40226,0.43022 0.80274,0.85777 1.22283,1.27821 l 35.75088,35.62612 c 1.88909,1.88174 2.71769,3.79638 2.69361,6.20968 l 0.003,0.20977 c 0.0527,2.43197 -0.76081,4.34571 -2.6276,6.20791 -1.44759,1.43909 -3.06286,2.27818 -4.9564,2.60262 12.81601,6.82123 26.89677,10.43184 41.05246,10.83362 2.80598,-5.92525 4.2509,-12.41848 4.29906,-19.43526 z" /> + <path fill="#ffffff" d="m 215.68093,93.181574 c 2.84701,-2.838179 7.46359,-2.836401 10.30883,0 2.84433,2.834623 2.84612,7.435446 -0.002,10.270956 -2.84345,2.83818 -7.46271,2.83818 -10.30704,0 -2.84524,-2.83551 -2.84791,-7.435444 0,-10.270956 z" /> + </g> + <g> + <g fill="#ffd43b"> + <path d="m 271.62046,177.33313 c 0,4.1637 1.46619,7.71227 4.39858,10.64361 2.9324,2.93556 6.48202,4.40069 10.64783,4.40069 4.16475,0 7.71438,-1.46513 10.64572,-4.40069 2.93344,-2.93134 4.40069,-6.47991 4.40069,-10.64361 v -35.00332 c 0,-2.12345 0.7647,-3.95198 2.29514,-5.48032 1.53045,-1.53256 3.35793,-2.29831 5.48349,-2.29831 h 0.12745 c 2.16664,0 3.972,0.76575 5.41923,2.29831 1.53045,1.52834 2.2962,3.35793 2.2962,5.48032 v 35.00332 c 0,4.1637 1.4662,7.71227 4.40069,10.64361 2.93134,2.93556 6.47886,4.40069 10.64572,4.40069 4.20794,0 7.77758,-1.46513 10.70997,-4.40069 2.93345,-2.93134 4.40069,-6.47991 4.40069,-10.64361 v -35.00332 c 0,-2.12345 0.76365,-3.95198 2.29515,-5.48032 1.44302,-1.53256 3.25049,-2.29831 5.41924,-2.29831 h 0.1264 c 2.12661,0 3.95409,0.76575 5.48349,2.29831 1.48831,1.52834 2.23194,3.35793 2.23194,5.48032 v 35.00332 c 0,8.45696 -2.9977,15.68261 -8.98887,21.67484 -5.99329,5.99224 -13.21999,8.98993 -21.67695,8.98993 -10.11696,0 -17.7239,-3.35583 -22.82609,-10.07272 -5.14222,6.71689 -12.77234,10.07272 -22.88719,10.07272 -8.45801,0 -15.68471,-2.99769 -21.67695,-8.98993 C 258.9998,193.01574 256,185.79113 256,177.33313 v -35.00332 c 0,-2.12345 0.76575,-3.95198 2.29619,-5.48032 1.5294,-1.53256 3.33581,-2.29831 5.41924,-2.29831 h 0.1917 c 2.08238,0 3.88774,0.76575 5.42029,2.29831 1.52834,1.52834 2.29409,3.35793 2.29409,5.48032 v 35.00332 z" /> + <path d="m 443.95216,155.97534 c 0.51085,1.06173 0.7668,2.14346 0.7668,3.25048 0,0.8932 -0.16957,1.78536 -0.50979,2.67854 -0.72363,1.99707 -2.08343,3.4422 -4.0805,4.33434 -5.95114,2.67854 -13.77085,6.20711 -23.46228,10.58463 -12.02871,5.43924 -19.08477,8.64866 -21.16715,9.62823 3.22943,4.07944 8.26737,6.11863 15.11067,6.11863 4.5471,0 8.67077,-1.33769 12.36786,-4.01625 3.61283,-2.63534 6.14286,-6.03541 7.58798,-10.20227 1.23342,-3.48538 3.69815,-5.22754 7.39524,-5.22754 2.6343,0 4.7388,1.10702 6.31138,3.31369 0.97746,1.36193 1.46619,2.78598 1.46619,4.27325 0,0.8932 -0.16958,1.80641 -0.50874,2.74069 -2.50791,7.26988 -6.90861,13.13573 -13.19681,17.59961 -6.37563,4.63031 -13.51702,6.94757 -21.4231,6.94757 -10.11591,0 -18.76563,-3.58965 -25.94809,-10.7742 -7.18351,-7.18353 -10.77527,-15.83219 -10.77527,-25.9502 0,-10.11591 3.59176,-18.76351 10.77527,-25.95019 7.18142,-7.1814 15.83218,-10.77422 25.94809,-10.77422 7.30885,0 13.98257,1.99916 20.01904,5.99223 5.99118,3.91512 10.43296,9.05524 13.32321,15.43298 z m -33.34331,-5.67836 c -5.86583,0 -10.86059,2.06343 -14.98322,6.18604 -4.08049,4.12473 -6.12073,9.11949 -6.12073,14.98322 v 0.44661 l 35.63951,-16.00282 c -3.1441,-3.73817 -7.99035,-5.61305 -14.53556,-5.61305 z" /> + <path d="m 465.12141,108.41246 c 2.08238,0 3.88775,0.74469 5.41924,2.23194 1.53045,1.52834 2.29619,3.35793 2.29619,5.48244 v 24.79998 c 4.80202,-4.24796 11.83701,-6.37564 21.10185,-6.37564 10.11591,0 18.76561,3.59177 25.94914,10.77422 7.18245,7.18563 10.77527,15.83429 10.77527,25.9502 0,10.11695 -3.59282,18.76561 -10.77527,25.95018 C 512.70536,204.41035 504.05566,208 493.93869,208 c -10.11696,0 -18.74349,-3.56964 -25.88382,-10.71207 -7.18457,-7.09504 -10.7974,-15.70262 -10.83954,-25.82063 v -55.33941 c 0,-2.12556 0.76576,-3.95409 2.29621,-5.48243 1.52939,-1.48727 3.3358,-2.23196 5.41924,-2.23196 h 0.19063 z m 28.81622,41.88452 c -5.86477,0 -10.85953,2.06343 -14.9832,6.18604 -4.0784,4.12473 -6.11969,9.11949 -6.11969,14.98322 0,5.8237 2.04129,10.79633 6.11969,14.91896 4.12367,4.12263 9.11737,6.18393 14.9832,6.18393 5.82371,0 10.79635,-2.0613 14.92002,-6.18393 4.12051,-4.12263 6.18288,-9.09526 6.18288,-14.91896 0,-5.86267 -2.06237,-10.85849 -6.18288,-14.98322 -4.12367,-4.12261 -9.09525,-6.18604 -14.92002,-6.18604 z" /> + </g> + <g fill="#306998"> + <path d="m 561.26467,150.17375 c -1.87066,0 -3.44325,0.6362 -4.71773,1.9107 -1.27556,1.31872 -1.91281,2.89025 -1.91281,4.71773 0,2.5511 1.23237,4.46389 3.69919,5.73733 0.84898,0.46872 4.39859,1.53045 10.64678,3.18834 5.05795,1.44619 8.81825,3.33686 11.28296,5.67413 3.52963,3.35898 5.29179,8.14097 5.29179,14.34703 0,6.11862 -2.16769,11.36829 -6.50203,15.74581 -4.37857,4.33644 -9.62823,6.50308 -15.74791,6.50308 h -16.64005 c -2.08448,0 -3.88879,-0.76365 -5.42029,-2.29621 -1.53045,-1.44407 -2.2962,-3.25048 -2.2962,-5.41712 v -0.12953 c 0,-2.12345 0.76575,-3.95198 2.2962,-5.48243 1.53044,-1.53045 3.33581,-2.29619 5.42029,-2.29619 h 17.2773 c 1.8696,0 3.44324,-0.6362 4.71773,-1.9107 1.27556,-1.27554 1.91281,-2.84707 1.91281,-4.71774 0,-2.33937 -1.21131,-4.10366 -3.63285,-5.29073 -0.63723,-0.30018 -4.20898,-1.36192 -10.71208,-3.18834 -5.05899,-1.48725 -8.82139,-3.44535 -11.28611,-5.8669 -3.52856,-3.44217 -5.29075,-8.30949 -5.29075,-14.5998 0,-6.12073 2.16876,-11.34721 6.50414,-15.68261 4.37648,-4.37752 9.62718,-6.56839 15.74687,-6.56839 h 11.73166 c 2.12452,0 3.95304,0.76575 5.48349,2.29831 1.52939,1.52834 2.29515,3.35793 2.29515,5.48032 v 0.12745 c 0,2.16876 -0.76576,3.97622 -2.29515,5.4203 -1.53045,1.52834 -3.35897,2.29619 -5.48349,2.29619 z" /> + <path d="m 630.5677,134.55118 c 10.1159,0 18.76456,3.59177 25.94912,10.77422 7.18246,7.18563 10.77422,15.83429 10.77422,25.9502 0,10.11695 -3.59176,18.76561 -10.77422,25.95018 C 649.33331,204.40929 640.6836,208 630.5677,208 c -10.11592,0 -18.76563,-3.58965 -25.9481,-10.77422 -7.18351,-7.18351 -10.77526,-15.83217 -10.77526,-25.95018 0,-10.11591 3.59175,-18.76352 10.77526,-25.9502 7.18247,-7.18245 15.83218,-10.77422 25.9481,-10.77422 z m 0,15.7458 c -5.86585,0 -10.86059,2.06343 -14.98322,6.18604 -4.08155,4.12473 -6.12178,9.11949 -6.12178,14.98322 0,5.8237 2.04023,10.79633 6.12178,14.91896 4.12263,4.12263 9.11632,6.18393 14.98322,6.18393 5.82264,0 10.79527,-2.0613 14.91896,-6.18393 4.12261,-4.12263 6.18393,-9.09526 6.18393,-14.91896 0,-5.86267 -2.06132,-10.85849 -6.18393,-14.98322 -4.12369,-4.12261 -9.09527,-6.18604 -14.91896,-6.18604 z" /> + <path d="m 724.0345,136.27333 c 3.61388,1.14811 5.4203,3.61282 5.4203,7.39523 v 0.32125 c 0,2.59008 -1.04278,4.65138 -3.12516,6.18394 -1.44512,1.01854 -2.93343,1.52834 -4.46178,1.52834 -0.80894,0 -1.63789,-0.12745 -2.48684,-0.38235 -2.08344,-0.67938 -4.23007,-1.02276 -6.43883,-1.02276 -5.86585,0 -10.86165,2.06343 -14.98322,6.18604 -4.08154,4.12473 -6.12074,9.11949 -6.12074,14.98322 0,5.8237 2.0392,10.79633 6.12074,14.91896 4.12157,4.12263 9.11633,6.18393 14.98322,6.18393 2.20982,0 4.35645,-0.33915 6.43883,-1.02065 0.80683,-0.25489 1.61471,-0.38234 2.42259,-0.38234 1.57046,0 3.08197,0.5119 4.52709,1.53254 2.08238,1.52835 3.12514,3.61283 3.12514,6.24819 0,3.74027 -1.80746,6.205 -5.42028,7.39524 -3.56964,1.10491 -7.26673,1.65579 -11.09232,1.65579 -10.11591,0 -18.76562,-3.58965 -25.95019,-10.77423 -7.1814,-7.18351 -10.77422,-15.83217 -10.77422,-25.95018 0,-10.11592 3.59176,-18.76352 10.77422,-25.9502 7.18351,-7.1814 15.83322,-10.77422 25.95019,-10.77422 3.82348,0.002 7.52162,0.57827 11.09126,1.72426 z" /> + <path d="m 748.19829,108.41246 c 2.08132,0 3.88773,0.74469 5.42029,2.23194 1.5294,1.52834 2.29514,3.35793 2.29514,5.48244 v 44.18284 h 2.42259 c 5.44031,0 10.17805,-1.80642 14.21852,-5.4203 3.95198,-3.61283 6.20394,-8.07461 6.75693,-13.38852 0.25491,-1.99705 1.10597,-3.63494 2.5511,-4.90837 1.44408,-1.35982 3.16517,-2.04131 5.16328,-2.04131 h 0.19066 c 2.25405,0 4.14578,0.85212 5.67517,2.55109 1.36087,1.48727 2.04026,3.20942 2.04026,5.16329 0,0.25491 -0.0222,0.53298 -0.0632,0.82895 -1.02064,10.66889 -5.10115,18.65923 -12.24147,23.97103 3.73922,2.29831 7.18246,6.18604 10.32973,11.66849 3.27155,5.65306 4.90944,11.75483 4.90944,18.29688 v 3.25471 c 0,2.16664 -0.7668,3.972 -2.29515,5.41713 -1.53255,1.53256 -3.33791,2.29619 -5.4203,2.29619 h -0.1917 c -2.08342,0 -3.88879,-0.76363 -5.41922,-2.29619 -1.53045,-1.44408 -2.29514,-3.25049 -2.29514,-5.41713 v -3.25471 c -0.0442,-5.77629 -2.10555,-10.73102 -6.185,-14.85575 -4.12367,-4.07944 -9.09736,-6.11863 -14.91896,-6.11863 h -5.22754 v 24.22804 c 0,2.16664 -0.76574,3.97199 -2.29514,5.41712 -1.5315,1.53256 -3.33897,2.29621 -5.42028,2.29621 h -0.19381 c -2.08237,0 -3.88668,-0.76365 -5.41819,-2.29621 -1.52939,-1.44407 -2.29515,-3.25048 -2.29515,-5.41712 v -84.15879 c 0,-2.12556 0.76576,-3.95408 2.29515,-5.48243 1.53045,-1.48727 3.33582,-2.23195 5.41819,-2.23195 h 0.19381 z" /> + <path d="m 876.85801,155.97534 c 0.5098,1.06173 0.76469,2.14346 0.76469,3.25048 0,0.8932 -0.17063,1.78536 -0.50874,2.67854 -0.72362,1.99707 -2.08342,3.4422 -4.08049,4.33434 -5.95115,2.67854 -13.77191,6.20711 -23.46229,10.58463 -12.02869,5.43924 -19.08476,8.64866 -21.16715,9.62823 3.22838,4.07944 8.26632,6.11863 15.11066,6.11863 4.54606,0 8.66973,-1.33769 12.36893,-4.01625 3.61176,-2.63534 6.14075,-6.03541 7.58587,-10.20227 1.23238,-3.48538 3.6992,-5.22754 7.39524,-5.22754 2.63536,0 4.73985,1.10702 6.31348,3.31369 0.97536,1.36193 1.46515,2.78598 1.46515,4.27325 0,0.8932 -0.16958,1.80641 -0.5098,2.74069 -2.50791,7.26988 -6.9065,13.13573 -13.19681,17.59961 -6.37563,4.63031 -13.51598,6.94757 -21.42206,6.94757 -10.1159,0 -18.76561,-3.58965 -25.94808,-10.7742 -7.18351,-7.18353 -10.77526,-15.83219 -10.77526,-25.9502 0,-10.11591 3.59175,-18.76351 10.77526,-25.95019 7.18141,-7.1814 15.83218,-10.77422 25.94808,-10.77422 7.30887,0 13.98364,1.99916 20.01906,5.99223 5.99223,3.91512 10.43294,9.05524 13.32426,15.43298 z m -33.34436,-5.67836 c -5.86479,0 -10.86059,2.06343 -14.98322,6.18604 -4.08049,4.12473 -6.12074,9.11949 -6.12074,14.98322 v 0.44661 l 35.63952,-16.00282 c -3.14516,-3.73817 -7.99034,-5.61305 -14.53556,-5.61305 z" /> + <path d="m 898.02411,108.41246 c 2.08238,0 3.88879,0.74469 5.42028,2.23194 1.52939,1.52834 2.29515,3.35793 2.29515,5.48244 v 18.42434 h 9.56398 c 2.08237,0 3.88772,0.76575 5.42028,2.29831 1.5294,1.52834 2.29304,3.35793 2.29304,5.48032 v 0.12745 c 0,2.16876 -0.76364,3.97621 -2.29304,5.4203 -1.5315,1.52834 -3.33791,2.29619 -5.42028,2.29619 h -9.56398 v 37.80405 c 0,1.23446 0.42343,2.27724 1.27554,3.12514 0.85002,0.85212 1.9128,1.27555 3.1873,1.27555 h 5.10114 c 2.08237,0 3.88772,0.76574 5.42028,2.29619 1.5294,1.53045 2.29304,3.35898 2.29304,5.48243 v 0.12954 c 0,2.16664 -0.76364,3.97199 -2.29304,5.41711 C 919.1923,207.23635 917.38589,208 915.30352,208 h -5.10114 c -5.52563,0 -10.26442,-1.95387 -14.21746,-5.86478 -3.91196,-3.95198 -5.86479,-8.67078 -5.86479,-14.15532 v -71.85095 c 0,-2.12558 0.7647,-3.9541 2.29515,-5.48245 1.53045,-1.48725 3.33686,-2.23193 5.41924,-2.23193 h 0.18959 z" /> + <path d="m 951.70877,150.17375 c -1.87066,0 -3.44324,0.6362 -4.71773,1.9107 -1.27556,1.31872 -1.91281,2.89025 -1.91281,4.71773 0,2.5511 1.23238,4.46389 3.69711,5.73733 0.8521,0.46872 4.40067,1.53045 10.64886,3.18834 5.05691,1.44619 8.81825,3.33686 11.28402,5.67413 3.52751,3.35898 5.2918,8.14097 5.2918,14.34703 0,6.11862 -2.16876,11.36829 -6.5031,15.74581 -4.37752,4.33644 -9.62822,6.50308 -15.74789,6.50308 h -16.64007 c -2.08342,0 -3.88879,-0.76365 -5.42028,-2.29621 -1.53045,-1.44407 -2.2941,-3.25048 -2.2941,-5.41712 v -0.12953 c 0,-2.12345 0.76365,-3.95198 2.2941,-5.48243 1.53045,-1.53045 3.33686,-2.29619 5.42028,-2.29619 h 17.2773 c 1.86962,0 3.4443,-0.6362 4.71775,-1.9107 1.27554,-1.27554 1.91279,-2.84707 1.91279,-4.71774 0,-2.33937 -1.2113,-4.10366 -3.63283,-5.29073 -0.63936,-0.30018 -4.209,-1.36192 -10.71208,-3.18834 -5.05901,-1.48725 -8.8214,-3.44535 -11.28613,-5.8669 -3.52856,-3.44217 -5.29073,-8.30949 -5.29073,-14.5998 0,-6.12073 2.16875,-11.34721 6.50413,-15.68261 4.37647,-4.37752 9.62718,-6.56839 15.74791,-6.56839 h 11.73063 c 2.1266,0 3.95304,0.76575 5.48243,2.29831 1.53045,1.52834 2.29514,3.35793 2.29514,5.48032 v 0.12745 c 0,2.16876 -0.76469,3.97622 -2.29514,5.4203 -1.52939,1.52834 -3.35687,2.29619 -5.48243,2.29619 z" /> + </g> + </g> +</svg> diff --git a/tests/wpt/tests/tools/third_party/websockets/logo/icon.html b/tests/wpt/tests/tools/third_party/websockets/logo/icon.html new file mode 100644 index 00000000000..6a71ec23bcf --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/logo/icon.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <title>Icon</title> + <style> + body { + font-family: -apple-system; + font-size: 24px; + font-weight: 100; + text-align: center; + } + </style> + </head> + <body> + <p>Take a screenshot of these DOM nodes to2x make a PNG.</p> + <p><img src="icon.svg" height="8" width="8" alt="8x8 / 16x16 @ 2x"></p> + <p><img src="icon.svg" height="16" width="16" alt="16x16 / 32x32 @ 2x"></p> + <p><img src="icon.svg" height="32" width="32" alt="32x32 / 32x32 @ 2x"></p> + <p><img src="icon.svg" height="32" width="32" alt="32x32 / 64x64 @ 2x"></p> + <p><img src="icon.svg" height="64" width="64" alt="64x64 / 128x128 @ 2x"></p> + <p><img src="icon.svg" height="128" width="128" alt="128x128 / 256x256 @ 2x"></p> + <p><img src="icon.svg" height="256" width="256" alt="256x256 / 512x512 @ 2x"></p> + <p><img src="icon.svg" height="512" width="512" alt="512x512 / 1024x1024 @ 2x"></p> + </body> +</html> diff --git a/tests/wpt/tests/tools/third_party/websockets/logo/icon.svg b/tests/wpt/tests/tools/third_party/websockets/logo/icon.svg new file mode 100644 index 00000000000..cb760940aa1 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/logo/icon.svg @@ -0,0 +1,15 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="256" height="256" viewBox="0 0 256 256"> + <linearGradient id="w" x1="0" y1="0" x2="0.6667" y2="0.6667"> + <stop offset="0%" stop-color="#ffe873" /> + <stop offset="100%" stop-color="#ffd43b" /> + </linearGradient> + <linearGradient id="s" x1="0" y1="0" x2="0.6667" y2="0.6667"> + <stop offset="0%" stop-color="#5a9fd4" /> + <stop offset="100%" stop-color="#306998" /> + </linearGradient> + <g> + <path fill="url(#w)" d="m 151.60708,154.81618 c -0.43704,0.0747 -0.88656,0.12978 -1.35572,0.14933 -2.45813,0.0764 -4.25357,-0.58665 -5.82335,-2.15107 l -8.89246,-8.85942 -11.23464,-11.19805 -36.040757,-35.919452 c -3.43568,-3.42217 -7.332485,-5.347474 -11.589626,-5.723468 -2.229803,-0.198219 -4.473877,0.03111 -6.640354,0.675545 -3.242133,0.944875 -6.135526,2.664848 -8.593662,5.116366 -3.834369,3.819499 -5.86349,8.414979 -5.875977,13.287799 -0.06065,4.95281 1.951523,9.60074 5.808192,13.44424 l 55.622894,55.43648 c 1.82219,1.84175 2.65971,3.79549 2.63384,6.14568 l 0.004,0.208 c 0.0527,2.43196 -0.75991,4.34571 -2.6267,6.20612 -1.78028,1.77598 -3.8094,2.65241 -6.30945,2.75552 -2.45814,0.0764 -4.25446,-0.58844 -5.82514,-2.15286 L 48.702551,136.2618 c -5.214172,-5.19459 -11.702899,-6.98745 -18.22998,-5.04881 -3.245701,0.9431 -6.135527,2.66307 -8.595446,5.11459 -3.83437,3.82127 -5.865275,8.41676 -5.875978,13.28957 -0.05619,4.95281 1.951524,9.60252 5.806409,13.4478 l 58.10689,57.90577 c 8.319842,8.29143 19.340421,11.9376 32.743314,10.83806 12.57967,-1.02043 23.02317,-5.5848 31.03441,-13.57313 7.51265,-7.4861 11.96423,-16.35175 13.28695,-26.42537 10.47206,-1.68264 19.29494,-6.04524 26.27512,-13.00158 4.01364,-3.99994 7.14963,-8.3972 9.40531,-13.16157 -14.15569,-0.39911 -28.23645,-4.00972 -41.05247,-10.83095 z" /> + <path fill="url(#s)" d="m 196.96038,146.11854 c 0.10259,-12.84514 -4.43017,-23.98541 -13.50635,-33.1346 L 147.57292,77.225374 c -0.24349,-0.240885 -0.46469,-0.487992 -0.68678,-0.744877 -1.48416,-1.739529 -2.18788,-3.583056 -2.21018,-5.807022 -0.0259,-2.470184 0.84911,-4.508375 2.7605,-6.407902 1.91406,-1.909304 3.8531,-2.737735 6.36564,-2.684403 2.53662,0.024 4.62728,0.943097 6.57257,2.881734 l 60.59178,60.384846 12.11408,-12.06914 c 1.12203,-0.90755 1.95777,-1.76887 2.87823,-2.93418 5.91879,-7.51544 5.26947,-18.272609 -1.51003,-25.02895 L 187.20456,37.727314 c -9.19393,-9.157192 -20.36703,-13.776677 -33.16789,-13.7269 -12.94266,-0.05067 -24.14163,4.548375 -33.28739,13.662901 -9.02892,8.996307 -13.64015,19.93925 -13.7008,32.487501 l -0.004,0.14222 c -0.002,0.167998 -0.005,0.336884 -0.005,0.506659 -0.091,12.232701 4.10729,22.95787 12.48154,31.881285 0.40226,0.43022 0.80274,0.85777 1.22283,1.27821 l 35.75088,35.62612 c 1.88909,1.88174 2.71769,3.79638 2.69361,6.20968 l 0.003,0.20977 c 0.0527,2.43197 -0.76081,4.34571 -2.6276,6.20791 -1.44759,1.43909 -3.06286,2.27818 -4.9564,2.60262 12.81601,6.82123 26.89677,10.43184 41.05246,10.83362 2.80598,-5.92525 4.2509,-12.41848 4.29906,-19.43526 z" /> + <path fill="#ffffff" d="m 215.68093,93.181574 c 2.84701,-2.838179 7.46359,-2.836401 10.30883,0 2.84433,2.834623 2.84612,7.435446 -0.002,10.270956 -2.84345,2.83818 -7.46271,2.83818 -10.30704,0 -2.84524,-2.83551 -2.84791,-7.435444 0,-10.270956 z" /> + </g> +</svg> diff --git a/tests/wpt/tests/tools/third_party/websockets/logo/old.svg b/tests/wpt/tests/tools/third_party/websockets/logo/old.svg new file mode 100644 index 00000000000..a073139e331 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/logo/old.svg @@ -0,0 +1,14 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="360" height="120" viewBox="0 0 21 7"> + <linearGradient id="w" x1="0" y1="0" x2="1" y2="1"> + <stop offset="0%" stop-color="#5a9fd4" /> + <stop offset="100%" stop-color="#306998" /> + </linearGradient> + <linearGradient id="s" x1="0" y1="0" x2="1" y2="1"> + <stop offset="0%" stop-color="#ffe873" /> + <stop offset="100%" stop-color="#ffd43b" /> + </linearGradient> + <polyline fill="none" stroke="url(#w)" stroke-linecap="round" stroke-linejoin="round" + points="1,1 1,5 5,5 5,1 5,5 9,5 9,1"/> + <polyline fill="none" stroke="url(#s)" stroke-linecap="round" stroke-linejoin="round" + points="19,1 11,1 11,3 19,3 19,5 11,5"/> +</svg> diff --git a/tests/wpt/tests/tools/third_party/websockets/logo/tidelift.png b/tests/wpt/tests/tools/third_party/websockets/logo/tidelift.png Binary files differnew file mode 100644 index 00000000000..317dc4d9852 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/logo/tidelift.png diff --git a/tests/wpt/tests/tools/third_party/websockets/logo/vertical.svg b/tests/wpt/tests/tools/third_party/websockets/logo/vertical.svg new file mode 100644 index 00000000000..b07fb223873 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/logo/vertical.svg @@ -0,0 +1,31 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="480" height="320" viewBox="0 0 480 320"> + <linearGradient id="w" x1="0.2333" y1="0" x2="0.5889" y2="0.5333"> + <stop offset="0%" stop-color="#ffe873" /> + <stop offset="100%" stop-color="#ffd43b" /> + </linearGradient> + <linearGradient id="s" x1="0.2333" y1="0" x2="0.5889" y2="0.5333"> + <stop offset="0%" stop-color="#5a9fd4" /> + <stop offset="100%" stop-color="#306998" /> + </linearGradient> + <g> + <path fill="url(#w)" d="m 263.40708,146.81618 c -0.43704,0.0747 -0.88656,0.12978 -1.35572,0.14933 -2.45813,0.0764 -4.25357,-0.58665 -5.82335,-2.15107 l -8.89246,-8.85942 -11.23464,-11.19805 -36.04076,-35.919454 c -3.43568,-3.42217 -7.33248,-5.347474 -11.58962,-5.723468 -2.22981,-0.198219 -4.47388,0.03111 -6.64036,0.675545 -3.24213,0.944875 -6.13552,2.664848 -8.59366,5.116366 -3.83437,3.819499 -5.86349,8.414979 -5.87598,13.287801 -0.0607,4.95281 1.95153,9.60074 5.8082,13.44424 l 55.62289,55.43648 c 1.82219,1.84175 2.65971,3.79549 2.63384,6.14568 l 0.004,0.208 c 0.0527,2.43196 -0.75991,4.34571 -2.6267,6.20612 -1.78028,1.77598 -3.8094,2.65241 -6.30945,2.75552 -2.45814,0.0764 -4.25446,-0.58844 -5.82514,-2.15286 L 160.50255,128.2618 c -5.21417,-5.19459 -11.7029,-6.98745 -18.22998,-5.04881 -3.2457,0.9431 -6.13553,2.66307 -8.59545,5.11459 -3.83437,3.82127 -5.86527,8.41676 -5.87597,13.28957 -0.0562,4.95281 1.95152,9.60252 5.80641,13.4478 l 58.10689,57.90577 c 8.31984,8.29143 19.34042,11.9376 32.74331,10.83806 12.57967,-1.02043 23.02317,-5.5848 31.03441,-13.57313 7.51265,-7.4861 11.96423,-16.35175 13.28695,-26.42537 10.47206,-1.68264 19.29494,-6.04524 26.27512,-13.00158 4.01364,-3.99994 7.14963,-8.3972 9.40531,-13.16157 -14.15569,-0.39911 -28.23645,-4.00972 -41.05247,-10.83095 z" /> + <path fill="url(#s)" d="m 308.76038,138.11854 c 0.10259,-12.84514 -4.43017,-23.98541 -13.50635,-33.1346 L 259.37292,69.225372 c -0.24349,-0.240885 -0.46469,-0.487992 -0.68678,-0.744877 -1.48416,-1.739529 -2.18788,-3.583056 -2.21018,-5.807022 -0.0259,-2.470184 0.84911,-4.508375 2.7605,-6.407902 1.91406,-1.909304 3.8531,-2.737735 6.36564,-2.684403 2.53662,0.024 4.62728,0.943097 6.57257,2.881734 l 60.59178,60.384848 12.11408,-12.06914 c 1.12203,-0.90755 1.95777,-1.76887 2.87823,-2.93418 5.91879,-7.515442 5.26947,-18.272611 -1.51003,-25.028952 L 299.00456,29.727312 c -9.19393,-9.157192 -20.36703,-13.776677 -33.16789,-13.7269 -12.94266,-0.05067 -24.14163,4.548375 -33.28739,13.662901 -9.02892,8.996307 -13.64015,19.93925 -13.7008,32.487501 l -0.004,0.14222 c -0.002,0.167998 -0.005,0.336884 -0.005,0.506659 -0.091,12.232701 4.10729,22.95787 12.48154,31.881285 0.40226,0.43022 0.80274,0.85777 1.22283,1.27821 l 35.75088,35.626122 c 1.88909,1.88174 2.71769,3.79638 2.69361,6.20968 l 0.003,0.20977 c 0.0527,2.43197 -0.76081,4.34571 -2.6276,6.20791 -1.44759,1.43909 -3.06286,2.27818 -4.9564,2.60262 12.81601,6.82123 26.89677,10.43184 41.05246,10.83362 2.80598,-5.92525 4.2509,-12.41848 4.29906,-19.43526 z" /> + <path fill="#ffffff" d="m 327.48093,85.181572 c 2.84701,-2.838179 7.46359,-2.836401 10.30883,0 2.84433,2.834623 2.84612,7.435446 -0.002,10.270956 -2.84345,2.83818 -7.46271,2.83818 -10.30704,0 -2.84524,-2.83551 -2.84791,-7.435444 0,-10.270956 z" /> + </g> + <g> + <g fill="#ffd43b"> + <path d="m 25.719398,284.91839 c 0,2.59075 0.912299,4.79875 2.736898,6.62269 1.824599,1.82657 4.033255,2.73821 6.625313,2.73821 2.591402,0 4.800058,-0.91164 6.624002,-2.73821 1.825254,-1.82394 2.738209,-4.03194 2.738209,-6.62269 v -21.77984 c 0,-1.32126 0.475811,-2.45901 1.42809,-3.40998 0.952278,-0.95359 2.089375,-1.43006 3.411947,-1.43006 h 0.0793 c 1.348132,0 2.471467,0.47647 3.371969,1.43006 0.952278,0.95097 1.428745,2.08938 1.428745,3.40998 v 21.77984 c 0,2.59075 0.912299,4.79875 2.738209,6.62269 1.823944,1.82657 4.031289,2.73821 6.624002,2.73821 2.618274,0 4.839382,-0.91164 6.663981,-2.73821 1.825254,-1.82394 2.738209,-4.03194 2.738209,-6.62269 v -21.77984 c 0,-1.32126 0.475156,-2.45901 1.42809,-3.40998 0.897881,-0.95359 2.022526,-1.43006 3.371969,-1.43006 h 0.07865 c 1.323228,0 2.460325,0.47647 3.411948,1.43006 0.926062,0.95097 1.388766,2.08938 1.388766,3.40998 v 21.77984 c 0,5.26211 -1.865233,9.75807 -5.593077,13.48657 -3.729156,3.7285 -8.22577,5.59373 -13.487876,5.59373 -6.294998,0 -11.028207,-2.08807 -14.202904,-6.26747 -3.199602,4.1794 -7.94723,6.26747 -14.240916,6.26747 -5.262763,0 -9.759377,-1.86523 -13.487876,-5.59373 C 17.866544,294.67646 16,290.18115 16,284.91839 v -21.77984 c 0,-1.32126 0.476467,-2.45901 1.428745,-3.40998 0.951623,-0.95359 2.075612,-1.43006 3.371969,-1.43006 h 0.11928 c 1.295702,0 2.419036,0.47647 3.372625,1.43006 0.950967,0.95097 1.427434,2.08938 1.427434,3.40998 v 21.77984 z" /> + <path d="m 132.94801,271.6291 c 0.31786,0.66063 0.47712,1.33371 0.47712,2.02252 0,0.55577 -0.10551,1.11089 -0.3172,1.66665 -0.45026,1.24262 -1.29636,2.14181 -2.53898,2.69692 -3.70293,1.66665 -8.56853,3.8622 -14.59875,6.58599 -7.48453,3.38442 -11.87497,5.38139 -13.17067,5.9909 2.00942,2.53832 5.14414,3.80715 9.40219,3.80715 2.82931,0 5.39515,-0.83234 7.69556,-2.499 2.24798,-1.63977 3.82222,-3.75537 4.72141,-6.34808 0.76746,-2.16868 2.30107,-3.25269 4.60148,-3.25269 1.63912,0 2.94859,0.68881 3.92708,2.06185 0.6082,0.84742 0.9123,1.7335 0.9123,2.65891 0,0.55577 -0.10552,1.12399 -0.31655,1.70532 -1.56048,4.52348 -4.29869,8.17334 -8.21135,10.95087 -3.96706,2.88108 -8.41059,4.32293 -13.32993,4.32293 -6.29434,0 -11.67639,-2.23356 -16.145474,-6.70395 -4.469743,-4.46975 -6.704615,-9.85114 -6.704615,-16.14679 0,-6.29434 2.234872,-11.67507 6.704615,-16.14678 4.468434,-4.46843 9.851134,-6.70396 16.145474,-6.70396 4.54773,0 8.70027,1.24392 12.45629,3.7285 3.72785,2.43607 6.49162,5.63437 8.29,9.60274 z m -20.74695,-3.5332 c -3.64985,0 -6.7577,1.28391 -9.32289,3.84909 -2.53897,2.5665 -3.808452,5.67435 -3.808452,9.32289 v 0.27789 l 22.175692,-9.95731 c -1.95633,-2.32597 -4.97177,-3.49256 -9.04435,-3.49256 z" /> + <path d="m 146.11999,242.03442 c 1.2957,0 2.41904,0.46336 3.37197,1.38876 0.95228,0.95097 1.42874,2.08938 1.42874,3.4113 v 15.4311 c 2.98792,-2.64318 7.36525,-3.96707 13.13004,-3.96707 6.29434,0 11.67638,2.23488 16.14613,6.70396 4.46908,4.47106 6.70461,9.85245 6.70461,16.14679 0,6.29499 -2.23553,11.67638 -6.70461,16.14678 -4.46909,4.4704 -9.85113,6.70396 -16.14613,6.70396 -6.295,0 -11.66262,-2.22111 -16.10549,-6.66529 -4.4704,-4.41469 -6.71838,-9.77052 -6.7446,-16.06617 v -34.43341 c 0,-1.32257 0.47647,-2.46032 1.42875,-3.41129 0.95162,-0.92541 2.07561,-1.38877 3.37197,-1.38877 h 0.11862 z m 17.93009,26.06148 c -3.64919,0 -6.75704,1.28391 -9.32288,3.84909 -2.53767,2.5665 -3.80781,5.67435 -3.80781,9.32289 0,3.62364 1.27014,6.71772 3.80781,9.28291 2.56584,2.56519 5.67303,3.84778 9.32288,3.84778 3.62364,0 6.71773,-1.28259 9.28357,-3.84778 2.56387,-2.56519 3.84712,-5.65927 3.84712,-9.28291 0,-3.64788 -1.28325,-6.75639 -3.84712,-9.32289 -2.56584,-2.56518 -5.65927,-3.84909 -9.28357,-3.84909 z" /> + </g> + <g fill="#306998"> + <path d="m 205.94246,268.01922 c -1.16397,0 -2.14247,0.39586 -2.93548,1.18888 -0.79368,0.82054 -1.19019,1.79838 -1.19019,2.93548 0,1.58735 0.76681,2.77753 2.30172,3.56989 0.52825,0.29165 2.7369,0.95228 6.62466,1.98386 3.14717,0.89985 5.48691,2.07627 7.02051,3.53057 2.19621,2.09003 3.29267,5.06549 3.29267,8.92704 0,3.80714 -1.34879,7.0736 -4.04571,9.79739 -2.72444,2.69823 -5.9909,4.04636 -9.7987,4.04636 h -10.35381 c -1.29701,0 -2.41969,-0.47516 -3.37262,-1.42875 -0.95228,-0.89853 -1.42875,-2.02252 -1.42875,-3.37065 v -0.0806 c 0,-1.32126 0.47647,-2.45901 1.42875,-3.41129 0.95227,-0.95228 2.07561,-1.42874 3.37262,-1.42874 h 10.75032 c 1.16331,0 2.14246,-0.39586 2.93548,-1.18888 0.79368,-0.79367 1.19019,-1.77151 1.19019,-2.93548 0,-1.45561 -0.7537,-2.55339 -2.26044,-3.29201 -0.3965,-0.18678 -2.61892,-0.84742 -6.66529,-1.98386 -3.14782,-0.9254 -5.48887,-2.14377 -7.02247,-3.65051 -2.19555,-2.1418 -3.29202,-5.17035 -3.29202,-9.08432 0,-3.80846 1.34945,-7.06049 4.04702,-9.75807 2.72314,-2.72379 5.99024,-4.087 9.79805,-4.087 h 7.2997 c 1.32192,0 2.45967,0.47647 3.41195,1.43006 0.95162,0.95097 1.42809,2.08938 1.42809,3.40998 v 0.0793 c 0,1.34945 -0.47647,2.47409 -1.42809,3.37263 -0.95228,0.95097 -2.09003,1.42874 -3.41195,1.42874 z" /> + <path d="m 249.06434,258.29851 c 6.29434,0 11.67573,2.23488 16.14612,6.70396 4.46909,4.47106 6.70396,9.85245 6.70396,16.14679 0,6.29499 -2.23487,11.67638 -6.70396,16.14678 -4.46974,4.46974 -9.85178,6.70396 -16.14612,6.70396 -6.29435,0 -11.67639,-2.23356 -16.14548,-6.70396 -4.46974,-4.46974 -6.70461,-9.85113 -6.70461,-16.14678 0,-6.29434 2.23487,-11.67508 6.70461,-16.14679 4.46909,-4.46908 9.85113,-6.70396 16.14548,-6.70396 z m 0,9.79739 c -3.64986,0 -6.7577,1.28391 -9.32289,3.84909 -2.53963,2.5665 -3.80911,5.67435 -3.80911,9.32289 0,3.62364 1.26948,6.71772 3.80911,9.28291 2.56519,2.56519 5.67238,3.84778 9.32289,3.84778 3.62298,0 6.71706,-1.28259 9.28291,-3.84778 2.56518,-2.56519 3.84778,-5.65927 3.84778,-9.28291 0,-3.64788 -1.2826,-6.75639 -3.84778,-9.32289 -2.56585,-2.56518 -5.65928,-3.84909 -9.28291,-3.84909 z" /> + <path d="m 307.22146,259.37007 c 2.24864,0.71438 3.37263,2.24798 3.37263,4.60148 v 0.19989 c 0,1.6116 -0.64884,2.89419 -1.94454,3.84778 -0.89919,0.63376 -1.82525,0.95097 -2.77622,0.95097 -0.50334,0 -1.01913,-0.0793 -1.54737,-0.23791 -1.29636,-0.42272 -2.63204,-0.63638 -4.00638,-0.63638 -3.64986,0 -6.75836,1.28391 -9.32289,3.84909 -2.53963,2.5665 -3.80846,5.67435 -3.80846,9.32289 0,3.62364 1.26883,6.71772 3.80846,9.28291 2.56453,2.56519 5.67238,3.84778 9.32289,3.84778 1.375,0 2.71068,-0.21103 4.00638,-0.63507 0.50203,-0.1586 1.00471,-0.2379 1.50739,-0.2379 0.97718,0 1.91767,0.31851 2.81686,0.95358 1.2957,0.95097 1.94453,2.24798 1.94453,3.88776 0,2.32728 -1.12464,3.86089 -3.37262,4.60148 -2.22111,0.6875 -4.52152,1.03027 -6.90189,1.03027 -6.29434,0 -11.67638,-2.23356 -16.14678,-6.70396 -4.46843,-4.46974 -6.70396,-9.85113 -6.70396,-16.14678 0,-6.29435 2.23487,-11.67508 6.70396,-16.14679 4.46974,-4.46843 9.85178,-6.70396 16.14678,-6.70396 2.37906,0.001 4.68012,0.35981 6.90123,1.07287 z" /> + <path d="m 322.25671,242.03442 c 1.29504,0 2.41903,0.46336 3.37262,1.38876 0.95163,0.95097 1.42809,2.08938 1.42809,3.4113 v 27.49154 h 1.50739 c 3.38508,0 6.33301,-1.12399 8.84708,-3.37263 2.45901,-2.24798 3.86023,-5.0242 4.20431,-8.33063 0.15861,-1.24261 0.68816,-2.26174 1.58735,-3.0541 0.89854,-0.84611 1.96944,-1.27015 3.21271,-1.27015 h 0.11863 c 1.40252,0 2.5796,0.53021 3.53122,1.58735 0.84676,0.92541 1.26949,1.99697 1.26949,3.21271 0,0.15861 -0.0138,0.33163 -0.0393,0.51579 -0.63507,6.63842 -3.17405,11.61019 -7.61692,14.91531 2.32663,1.43006 4.46909,3.84909 6.42739,7.26039 2.03563,3.51746 3.05476,7.31412 3.05476,11.38473 v 2.02515 c 0,1.34813 -0.47712,2.47147 -1.42809,3.37066 -0.95359,0.95359 -2.07692,1.42874 -3.37263,1.42874 h -0.11928 c -1.29635,0 -2.41969,-0.47515 -3.37196,-1.42874 -0.95228,-0.89854 -1.42809,-2.02253 -1.42809,-3.37066 v -2.02515 c -0.0275,-3.59414 -1.31012,-6.67708 -3.84844,-9.24358 -2.56584,-2.53832 -5.66058,-3.80715 -9.28291,-3.80715 h -3.25269 v 15.07523 c 0,1.34813 -0.47646,2.47146 -1.42809,3.37065 -0.95293,0.95359 -2.07758,1.42875 -3.37262,1.42875 h -0.12059 c -1.2957,0 -2.41838,-0.47516 -3.37132,-1.42875 -0.95162,-0.89853 -1.42809,-2.02252 -1.42809,-3.37065 v -52.36547 c 0,-1.32257 0.47647,-2.46032 1.42809,-3.41129 0.95228,-0.92541 2.07562,-1.38877 3.37132,-1.38877 h 0.12059 z" /> + <path d="m 402.31164,271.6291 c 0.31721,0.66063 0.47581,1.33371 0.47581,2.02252 0,0.55577 -0.10617,1.11089 -0.31655,1.66665 -0.45025,1.24262 -1.29635,2.14181 -2.53897,2.69692 -3.70294,1.66665 -8.56919,3.8622 -14.59876,6.58599 -7.48452,3.38442 -11.87496,5.38139 -13.17067,5.9909 2.00877,2.53832 5.14349,3.80715 9.40219,3.80715 2.82866,0 5.3945,-0.83234 7.69622,-2.499 2.24732,-1.63977 3.82091,-3.75537 4.7201,-6.34808 0.76681,-2.16868 2.30172,-3.25269 4.60148,-3.25269 1.63978,0 2.94924,0.68881 3.92839,2.06185 0.60689,0.84742 0.91165,1.7335 0.91165,2.65891 0,0.55577 -0.10552,1.12399 -0.31721,1.70532 -1.56048,4.52348 -4.29738,8.17334 -8.21135,10.95087 -3.96706,2.88108 -8.40994,4.32293 -13.32928,4.32293 -6.29434,0 -11.67638,-2.23356 -16.14547,-6.70395 -4.46974,-4.46975 -6.70461,-9.85114 -6.70461,-16.14679 0,-6.29434 2.23487,-11.67507 6.70461,-16.14678 4.46843,-4.46843 9.85113,-6.70396 16.14547,-6.70396 4.54774,0 8.70093,1.24392 12.4563,3.7285 3.7285,2.43607 6.49161,5.63437 8.29065,9.60274 z m -20.7476,-3.5332 c -3.6492,0 -6.7577,1.28391 -9.32289,3.84909 -2.53897,2.5665 -3.80846,5.67435 -3.80846,9.32289 v 0.27789 l 22.1757,-9.95731 c -1.95699,-2.32597 -4.97177,-3.49256 -9.04435,-3.49256 z" /> + <path d="m 415.48166,242.03442 c 1.2957,0 2.41969,0.46336 3.37262,1.38876 0.95162,0.95097 1.42809,2.08938 1.42809,3.4113 v 11.46403 h 5.95092 c 1.2957,0 2.41903,0.47647 3.37262,1.43006 0.95163,0.95097 1.42678,2.08938 1.42678,3.40998 v 0.0793 c 0,1.34945 -0.47515,2.47409 -1.42678,3.37263 -0.95293,0.95097 -2.07692,1.42874 -3.37262,1.42874 h -5.95092 v 23.52252 c 0,0.76811 0.26347,1.41695 0.79367,1.94453 0.5289,0.53021 1.19019,0.79368 1.98321,0.79368 h 3.17404 c 1.2957,0 2.41903,0.47646 3.37262,1.42874 0.95163,0.95228 1.42678,2.09003 1.42678,3.41129 v 0.0806 c 0,1.34813 -0.47515,2.47146 -1.42678,3.37065 C 428.65298,303.52484 427.52899,304 426.23329,304 h -3.17404 c -3.43817,0 -6.38675,-1.21574 -8.84642,-3.6492 -2.43411,-2.45901 -3.6492,-5.39515 -3.6492,-8.80775 v -44.70726 c 0,-1.32258 0.47581,-2.46033 1.42809,-3.4113 0.95228,-0.9254 2.07627,-1.38876 3.37197,-1.38876 h 0.11797 z" /> + <path d="m 448.88545,268.01922 c -1.16397,0 -2.14246,0.39586 -2.93548,1.18888 -0.79368,0.82054 -1.19019,1.79838 -1.19019,2.93548 0,1.58735 0.76681,2.77753 2.30042,3.56989 0.5302,0.29165 2.7382,0.95228 6.62596,1.98386 3.14652,0.89985 5.48691,2.07627 7.02117,3.53057 2.19489,2.09003 3.29267,5.06549 3.29267,8.92704 0,3.80714 -1.34945,7.0736 -4.04637,9.79739 -2.72379,2.69823 -5.99089,4.04636 -9.79869,4.04636 h -10.35382 c -1.29635,0 -2.41969,-0.47516 -3.37262,-1.42875 -0.95228,-0.89853 -1.42744,-2.02252 -1.42744,-3.37065 v -0.0806 c 0,-1.32126 0.47516,-2.45901 1.42744,-3.41129 0.95228,-0.95228 2.07627,-1.42874 3.37262,-1.42874 h 10.75032 c 1.16332,0 2.14312,-0.39586 2.93549,-1.18888 0.79367,-0.79367 1.19018,-1.77151 1.19018,-2.93548 0,-1.45561 -0.7537,-2.55339 -2.26043,-3.29201 -0.39782,-0.18678 -2.61893,-0.84742 -6.66529,-1.98386 -3.14783,-0.9254 -5.48887,-2.14377 -7.02248,-3.65051 -2.19555,-2.1418 -3.29201,-5.17035 -3.29201,-9.08432 0,-3.80846 1.34944,-7.06049 4.04701,-9.75807 2.72314,-2.72379 5.99025,-4.087 9.7987,-4.087 h 7.29906 c 1.32322,0 2.45967,0.47647 3.41129,1.43006 0.95228,0.95097 1.42809,2.08938 1.42809,3.40998 v 0.0793 c 0,1.34945 -0.47581,2.47409 -1.42809,3.37263 -0.95162,0.95097 -2.08872,1.42874 -3.41129,1.42874 z" /> + </g> + </g> +</svg> diff --git a/tests/wpt/tests/tools/third_party/websockets/pyproject.toml b/tests/wpt/tests/tools/third_party/websockets/pyproject.toml new file mode 100644 index 00000000000..f24616dd7ea --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/pyproject.toml @@ -0,0 +1,87 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "websockets" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +requires-python = ">=3.8" +license = { text = "BSD-3-Clause" } +authors = [ + { name = "Aymeric Augustin", email = "aymeric.augustin@m4x.org" }, +] +keywords = ["WebSocket"] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", +] +dynamic = ["version", "readme"] + +[project.urls] +Homepage = "https://github.com/python-websockets/websockets" +Changelog = "https://websockets.readthedocs.io/en/stable/project/changelog.html" +Documentation = "https://websockets.readthedocs.io/" +Funding = "https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=readme" +Tracker = "https://github.com/python-websockets/websockets/issues" + +# On a macOS runner, build Intel, Universal, and Apple Silicon wheels. +[tool.cibuildwheel.macos] +archs = ["x86_64", "universal2", "arm64"] + +# On an Linux Intel runner with QEMU installed, build Intel and ARM wheels. +[tool.cibuildwheel.linux] +archs = ["auto", "aarch64"] + +[tool.coverage.run] +branch = true +omit = [ + # */websockets matches src/websockets and .tox/**/site-packages/websockets + "*/websockets/__main__.py", + "*/websockets/legacy/async_timeout.py", + "*/websockets/legacy/compatibility.py", + "tests/maxi_cov.py", +] + +[tool.coverage.paths] +source = [ + "src/websockets", + ".tox/*/lib/python*/site-packages/websockets", +] + +[tool.coverage.report] +exclude_lines = [ + "except ImportError:", + "if self.debug:", + "if sys.platform != \"win32\":", + "if typing.TYPE_CHECKING:", + "pragma: no cover", + "raise AssertionError", + "raise NotImplementedError", + "self.fail\\(\".*\"\\)", + "@unittest.skip", +] + +[tool.ruff] +select = [ + "E", # pycodestyle + "F", # Pyflakes + "W", # pycodestyle + "I", # isort +] +ignore = [ + "F403", + "F405", +] + +[tool.ruff.isort] +combine-as-imports = true +lines-after-imports = 2 diff --git a/tests/wpt/tests/tools/third_party/websockets/setup.cfg b/tests/wpt/tests/tools/third_party/websockets/setup.cfg deleted file mode 100644 index dc424fe1955..00000000000 --- a/tests/wpt/tests/tools/third_party/websockets/setup.cfg +++ /dev/null @@ -1,42 +0,0 @@ -[bdist_wheel] -python-tag = py37.py38.py39.py310 - -[metadata] -license_file = LICENSE -project_urls = - Changelog = https://websockets.readthedocs.io/en/stable/project/changelog.html - Documentation = https://websockets.readthedocs.io/ - Funding = https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=readme - Tracker = https://github.com/aaugustin/websockets/issues - -[flake8] -ignore = E203,E731,F403,F405,W503 -max-line-length = 88 - -[isort] -profile = black -combine_as_imports = True -lines_after_imports = 2 - -[coverage:run] -branch = True -omit = - */__main__.py -source = - websockets - tests - -[coverage:paths] -source = - src/websockets - .tox/*/lib/python*/site-packages/websockets - -[coverage:report] -exclude_lines = - if self.debug: - pragma: no cover - -[egg_info] -tag_build = -tag_date = 0 - diff --git a/tests/wpt/tests/tools/third_party/websockets/setup.py b/tests/wpt/tests/tools/third_party/websockets/setup.py index b2d07737d22..ae0aaa65de7 100644 --- a/tests/wpt/tests/tools/third_party/websockets/setup.py +++ b/tests/wpt/tests/tools/third_party/websockets/setup.py @@ -1,3 +1,4 @@ +import os import pathlib import re @@ -6,58 +7,32 @@ import setuptools root_dir = pathlib.Path(__file__).parent -description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +exec((root_dir / "src" / "websockets" / "version.py").read_text(encoding="utf-8")) -long_description = (root_dir / 'README.rst').read_text(encoding='utf-8') - -# PyPI disables the "raw" directive. +# PyPI disables the "raw" directive. Remove this section of the README. long_description = re.sub( r"^\.\. raw:: html.*?^(?=\w)", "", - long_description, + (root_dir / "README.rst").read_text(encoding="utf-8"), flags=re.DOTALL | re.MULTILINE, ) -exec((root_dir / 'src' / 'websockets' / 'version.py').read_text(encoding='utf-8')) - -packages = ['websockets', 'websockets/legacy', 'websockets/extensions'] - -ext_modules = [ - setuptools.Extension( - 'websockets.speedups', - sources=['src/websockets/speedups.c'], - optional=not (root_dir / '.cibuildwheel').exists(), - ) -] - +# Set BUILD_EXTENSION to yes or no to force building or not building the +# speedups extension. If unset, the extension is built only if possible. +if os.environ.get("BUILD_EXTENSION") == "no": + ext_modules = [] +else: + ext_modules = [ + setuptools.Extension( + "websockets.speedups", + sources=["src/websockets/speedups.c"], + optional=os.environ.get("BUILD_EXTENSION") != "yes", + ) + ] + +# Static values are declared in pyproject.toml. setuptools.setup( - name='websockets', version=version, - description=description, long_description=long_description, - url='https://github.com/aaugustin/websockets', - author='Aymeric Augustin', - author_email='aymeric.augustin@m4x.org', - license='BSD', - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Web Environment', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - ], - package_dir = {'': 'src'}, - package_data = {'websockets': ['py.typed']}, - packages=packages, ext_modules=ext_modules, - include_package_data=True, - zip_safe=False, - python_requires='>=3.7', - test_loader='unittest:TestLoader', ) diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets.egg-info/PKG-INFO b/tests/wpt/tests/tools/third_party/websockets/src/websockets.egg-info/PKG-INFO deleted file mode 100644 index 3b042a3f9f8..00000000000 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets.egg-info/PKG-INFO +++ /dev/null @@ -1,174 +0,0 @@ -Metadata-Version: 2.1 -Name: websockets -Version: 10.3 -Summary: An implementation of the WebSocket Protocol (RFC 6455 & 7692) -Home-page: https://github.com/aaugustin/websockets -Author: Aymeric Augustin -Author-email: aymeric.augustin@m4x.org -License: BSD -Project-URL: Changelog, https://websockets.readthedocs.io/en/stable/project/changelog.html -Project-URL: Documentation, https://websockets.readthedocs.io/ -Project-URL: Funding, https://tidelift.com/subscription/pkg/pypi-websockets?utm_source=pypi-websockets&utm_medium=referral&utm_campaign=readme -Project-URL: Tracker, https://github.com/aaugustin/websockets/issues -Platform: UNKNOWN -Classifier: Development Status :: 5 - Production/Stable -Classifier: Environment :: Web Environment -Classifier: Intended Audience :: Developers -Classifier: License :: OSI Approved :: BSD License -Classifier: Operating System :: OS Independent -Classifier: Programming Language :: Python -Classifier: Programming Language :: Python :: 3 -Classifier: Programming Language :: Python :: 3.7 -Classifier: Programming Language :: Python :: 3.8 -Classifier: Programming Language :: Python :: 3.9 -Classifier: Programming Language :: Python :: 3.10 -Requires-Python: >=3.7 -License-File: LICENSE - -.. image:: logo/horizontal.svg - :width: 480px - :alt: websockets - -|licence| |version| |pyversions| |wheel| |tests| |docs| - -.. |licence| image:: https://img.shields.io/pypi/l/websockets.svg - :target: https://pypi.python.org/pypi/websockets - -.. |version| image:: https://img.shields.io/pypi/v/websockets.svg - :target: https://pypi.python.org/pypi/websockets - -.. |pyversions| image:: https://img.shields.io/pypi/pyversions/websockets.svg - :target: https://pypi.python.org/pypi/websockets - -.. |wheel| image:: https://img.shields.io/pypi/wheel/websockets.svg - :target: https://pypi.python.org/pypi/websockets - -.. |tests| image:: https://img.shields.io/github/checks-status/aaugustin/websockets/main - :target: https://github.com/aaugustin/websockets/actions/workflows/tests.yml - -.. |docs| image:: https://img.shields.io/readthedocs/websockets.svg - :target: https://websockets.readthedocs.io/ - -What is ``websockets``? ------------------------ - -websockets is a library for building WebSocket_ servers and clients in Python -with a focus on correctness, simplicity, robustness, and performance. - -.. _WebSocket: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API - -Built on top of ``asyncio``, Python's standard asynchronous I/O framework, it -provides an elegant coroutine-based API. - -`Documentation is available on Read the Docs. <https://websockets.readthedocs.io/>`_ - -Here's how a client sends and receives messages: - -.. copy-pasted because GitHub doesn't support the include directive - -.. code:: python - - #!/usr/bin/env python - - import asyncio - from websockets import connect - - async def hello(uri): - async with connect(uri) as websocket: - await websocket.send("Hello world!") - await websocket.recv() - - asyncio.run(hello("ws://localhost:8765")) - -And here's an echo server: - -.. code:: python - - #!/usr/bin/env python - - import asyncio - from websockets import serve - - async def echo(websocket): - async for message in websocket: - await websocket.send(message) - - async def main(): - async with serve(echo, "localhost", 8765): - await asyncio.Future() # run forever - - asyncio.run(main()) - -Does that look good? - -`Get started with the tutorial! <https://websockets.readthedocs.io/en/stable/intro/index.html>`_ - -Why should I use ``websockets``? --------------------------------- - -The development of ``websockets`` is shaped by four principles: - -1. **Correctness**: ``websockets`` is heavily tested for compliance - with :rfc:`6455`. Continuous integration fails under 100% branch - coverage. - -2. **Simplicity**: all you need to understand is ``msg = await ws.recv()`` and - ``await ws.send(msg)``. ``websockets`` takes care of managing connections - so you can focus on your application. - -3. **Robustness**: ``websockets`` is built for production. For example, it was - the only library to `handle backpressure correctly`_ before the issue - became widely known in the Python community. - -4. **Performance**: memory usage is optimized and configurable. A C extension - accelerates expensive operations. It's pre-compiled for Linux, macOS and - Windows and packaged in the wheel format for each system and Python version. - -Documentation is a first class concern in the project. Head over to `Read the -Docs`_ and see for yourself. - -.. _Read the Docs: https://websockets.readthedocs.io/ -.. _handle backpressure correctly: https://vorpus.org/blog/some-thoughts-on-asynchronous-api-design-in-a-post-asyncawait-world/#websocket-servers - -Why shouldn't I use ``websockets``? ------------------------------------ - -* If you prefer callbacks over coroutines: ``websockets`` was created to - provide the best coroutine-based API to manage WebSocket connections in - Python. Pick another library for a callback-based API. - -* If you're looking for a mixed HTTP / WebSocket library: ``websockets`` aims - at being an excellent implementation of :rfc:`6455`: The WebSocket Protocol - and :rfc:`7692`: Compression Extensions for WebSocket. Its support for HTTP - is minimal — just enough for a HTTP health check. - - If you want to do both in the same server, look at HTTP frameworks that - build on top of ``websockets`` to support WebSocket connections, like - Sanic_. - -.. _Sanic: https://sanicframework.org/en/ - -What else? ----------- - -Bug reports, patches and suggestions are welcome! - -To report a security vulnerability, please use the `Tidelift security -contact`_. Tidelift will coordinate the fix and disclosure. - -.. _Tidelift security contact: https://tidelift.com/security - -For anything else, please open an issue_ or send a `pull request`_. - -.. _issue: https://github.com/aaugustin/websockets/issues/new -.. _pull request: https://github.com/aaugustin/websockets/compare/ - -Participants must uphold the `Contributor Covenant code of conduct`_. - -.. _Contributor Covenant code of conduct: https://github.com/aaugustin/websockets/blob/main/CODE_OF_CONDUCT.md - -``websockets`` is released under the `BSD license`_. - -.. _BSD license: https://github.com/aaugustin/websockets/blob/main/LICENSE - - diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets.egg-info/SOURCES.txt b/tests/wpt/tests/tools/third_party/websockets/src/websockets.egg-info/SOURCES.txt deleted file mode 100644 index 2a51106feeb..00000000000 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets.egg-info/SOURCES.txt +++ /dev/null @@ -1,42 +0,0 @@ -LICENSE -MANIFEST.in -README.rst -setup.cfg -setup.py -src/websockets/__init__.py -src/websockets/__main__.py -src/websockets/auth.py -src/websockets/client.py -src/websockets/connection.py -src/websockets/datastructures.py -src/websockets/exceptions.py -src/websockets/frames.py -src/websockets/headers.py -src/websockets/http.py -src/websockets/http11.py -src/websockets/imports.py -src/websockets/py.typed -src/websockets/server.py -src/websockets/speedups.c -src/websockets/streams.py -src/websockets/typing.py -src/websockets/uri.py -src/websockets/utils.py -src/websockets/version.py -src/websockets.egg-info/PKG-INFO -src/websockets.egg-info/SOURCES.txt -src/websockets.egg-info/dependency_links.txt -src/websockets.egg-info/not-zip-safe -src/websockets.egg-info/top_level.txt -src/websockets/extensions/__init__.py -src/websockets/extensions/base.py -src/websockets/extensions/permessage_deflate.py -src/websockets/legacy/__init__.py -src/websockets/legacy/auth.py -src/websockets/legacy/client.py -src/websockets/legacy/compatibility.py -src/websockets/legacy/framing.py -src/websockets/legacy/handshake.py -src/websockets/legacy/http.py -src/websockets/legacy/protocol.py -src/websockets/legacy/server.py
\ No newline at end of file diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets.egg-info/dependency_links.txt b/tests/wpt/tests/tools/third_party/websockets/src/websockets.egg-info/dependency_links.txt deleted file mode 100644 index 8b137891791..00000000000 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets.egg-info/dependency_links.txt +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets.egg-info/not-zip-safe b/tests/wpt/tests/tools/third_party/websockets/src/websockets.egg-info/not-zip-safe deleted file mode 100644 index 8b137891791..00000000000 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets.egg-info/not-zip-safe +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets.egg-info/top_level.txt b/tests/wpt/tests/tools/third_party/websockets/src/websockets.egg-info/top_level.txt deleted file mode 100644 index 5474af7431b..00000000000 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets.egg-info/top_level.txt +++ /dev/null @@ -1,3 +0,0 @@ -websockets -websockets/extensions -websockets/legacy diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/__init__.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/__init__.py index ec34841247b..fdb028f4c4f 100644 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets/__init__.py +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/__init__.py @@ -1,23 +1,24 @@ from __future__ import annotations +import typing + from .imports import lazy_import -from .version import version as __version__ # noqa +from .version import version as __version__ # noqa: F401 -__all__ = [ # noqa +__all__ = [ + # .client + "ClientProtocol", + # .datastructures + "Headers", + "HeadersLike", + "MultipleValuesError", + # .exceptions "AbortHandshake", - "basic_auth_protocol_factory", - "BasicAuthWebSocketServerProtocol", - "broadcast", - "ClientConnection", - "connect", "ConnectionClosed", "ConnectionClosedError", "ConnectionClosedOK", - "Data", "DuplicateParameter", - "ExtensionName", - "ExtensionParameter", "InvalidHandshake", "InvalidHeader", "InvalidHeaderFormat", @@ -31,84 +32,159 @@ __all__ = [ # noqa "InvalidStatusCode", "InvalidUpgrade", "InvalidURI", - "LoggerLike", "NegotiationError", - "Origin", - "parse_uri", "PayloadTooBig", "ProtocolError", "RedirectHandshake", "SecurityError", - "serve", - "ServerConnection", - "Subprotocol", - "unix_connect", - "unix_serve", - "WebSocketClientProtocol", - "WebSocketCommonProtocol", "WebSocketException", "WebSocketProtocolError", + # .legacy.auth + "BasicAuthWebSocketServerProtocol", + "basic_auth_protocol_factory", + # .legacy.client + "WebSocketClientProtocol", + "connect", + "unix_connect", + # .legacy.protocol + "WebSocketCommonProtocol", + "broadcast", + # .legacy.server "WebSocketServer", "WebSocketServerProtocol", - "WebSocketURI", + "serve", + "unix_serve", + # .server + "ServerProtocol", + # .typing + "Data", + "ExtensionName", + "ExtensionParameter", + "LoggerLike", + "StatusLike", + "Origin", + "Subprotocol", ] -lazy_import( - globals(), - aliases={ - "auth": ".legacy", - "basic_auth_protocol_factory": ".legacy.auth", - "BasicAuthWebSocketServerProtocol": ".legacy.auth", - "broadcast": ".legacy.protocol", - "ClientConnection": ".client", - "connect": ".legacy.client", - "unix_connect": ".legacy.client", - "WebSocketClientProtocol": ".legacy.client", - "Headers": ".datastructures", - "MultipleValuesError": ".datastructures", - "WebSocketException": ".exceptions", - "ConnectionClosed": ".exceptions", - "ConnectionClosedError": ".exceptions", - "ConnectionClosedOK": ".exceptions", - "InvalidHandshake": ".exceptions", - "SecurityError": ".exceptions", - "InvalidMessage": ".exceptions", - "InvalidHeader": ".exceptions", - "InvalidHeaderFormat": ".exceptions", - "InvalidHeaderValue": ".exceptions", - "InvalidOrigin": ".exceptions", - "InvalidUpgrade": ".exceptions", - "InvalidStatus": ".exceptions", - "InvalidStatusCode": ".exceptions", - "NegotiationError": ".exceptions", - "DuplicateParameter": ".exceptions", - "InvalidParameterName": ".exceptions", - "InvalidParameterValue": ".exceptions", - "AbortHandshake": ".exceptions", - "RedirectHandshake": ".exceptions", - "InvalidState": ".exceptions", - "InvalidURI": ".exceptions", - "PayloadTooBig": ".exceptions", - "ProtocolError": ".exceptions", - "WebSocketProtocolError": ".exceptions", - "protocol": ".legacy", - "WebSocketCommonProtocol": ".legacy.protocol", - "ServerConnection": ".server", - "serve": ".legacy.server", - "unix_serve": ".legacy.server", - "WebSocketServerProtocol": ".legacy.server", - "WebSocketServer": ".legacy.server", - "Data": ".typing", - "LoggerLike": ".typing", - "Origin": ".typing", - "ExtensionHeader": ".typing", - "ExtensionParameter": ".typing", - "Subprotocol": ".typing", - }, - deprecated_aliases={ - "framing": ".legacy", - "handshake": ".legacy", - "parse_uri": ".uri", - "WebSocketURI": ".uri", - }, -) +# When type checking, import non-deprecated aliases eagerly. Else, import on demand. +if typing.TYPE_CHECKING: + from .client import ClientProtocol + from .datastructures import Headers, HeadersLike, MultipleValuesError + from .exceptions import ( + AbortHandshake, + ConnectionClosed, + ConnectionClosedError, + ConnectionClosedOK, + DuplicateParameter, + InvalidHandshake, + InvalidHeader, + InvalidHeaderFormat, + InvalidHeaderValue, + InvalidMessage, + InvalidOrigin, + InvalidParameterName, + InvalidParameterValue, + InvalidState, + InvalidStatus, + InvalidStatusCode, + InvalidUpgrade, + InvalidURI, + NegotiationError, + PayloadTooBig, + ProtocolError, + RedirectHandshake, + SecurityError, + WebSocketException, + WebSocketProtocolError, + ) + from .legacy.auth import ( + BasicAuthWebSocketServerProtocol, + basic_auth_protocol_factory, + ) + from .legacy.client import WebSocketClientProtocol, connect, unix_connect + from .legacy.protocol import WebSocketCommonProtocol, broadcast + from .legacy.server import ( + WebSocketServer, + WebSocketServerProtocol, + serve, + unix_serve, + ) + from .server import ServerProtocol + from .typing import ( + Data, + ExtensionName, + ExtensionParameter, + LoggerLike, + Origin, + StatusLike, + Subprotocol, + ) +else: + lazy_import( + globals(), + aliases={ + # .client + "ClientProtocol": ".client", + # .datastructures + "Headers": ".datastructures", + "HeadersLike": ".datastructures", + "MultipleValuesError": ".datastructures", + # .exceptions + "AbortHandshake": ".exceptions", + "ConnectionClosed": ".exceptions", + "ConnectionClosedError": ".exceptions", + "ConnectionClosedOK": ".exceptions", + "DuplicateParameter": ".exceptions", + "InvalidHandshake": ".exceptions", + "InvalidHeader": ".exceptions", + "InvalidHeaderFormat": ".exceptions", + "InvalidHeaderValue": ".exceptions", + "InvalidMessage": ".exceptions", + "InvalidOrigin": ".exceptions", + "InvalidParameterName": ".exceptions", + "InvalidParameterValue": ".exceptions", + "InvalidState": ".exceptions", + "InvalidStatus": ".exceptions", + "InvalidStatusCode": ".exceptions", + "InvalidUpgrade": ".exceptions", + "InvalidURI": ".exceptions", + "NegotiationError": ".exceptions", + "PayloadTooBig": ".exceptions", + "ProtocolError": ".exceptions", + "RedirectHandshake": ".exceptions", + "SecurityError": ".exceptions", + "WebSocketException": ".exceptions", + "WebSocketProtocolError": ".exceptions", + # .legacy.auth + "BasicAuthWebSocketServerProtocol": ".legacy.auth", + "basic_auth_protocol_factory": ".legacy.auth", + # .legacy.client + "WebSocketClientProtocol": ".legacy.client", + "connect": ".legacy.client", + "unix_connect": ".legacy.client", + # .legacy.protocol + "WebSocketCommonProtocol": ".legacy.protocol", + "broadcast": ".legacy.protocol", + # .legacy.server + "WebSocketServer": ".legacy.server", + "WebSocketServerProtocol": ".legacy.server", + "serve": ".legacy.server", + "unix_serve": ".legacy.server", + # .server + "ServerProtocol": ".server", + # .typing + "Data": ".typing", + "ExtensionName": ".typing", + "ExtensionParameter": ".typing", + "LoggerLike": ".typing", + "Origin": ".typing", + "StatusLike": "typing", + "Subprotocol": ".typing", + }, + deprecated_aliases={ + "framing": ".legacy", + "handshake": ".legacy", + "parse_uri": ".uri", + "WebSocketURI": ".uri", + }, + ) diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/__main__.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/__main__.py index c562d21b544..f2ea5cf4e8f 100644 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets/__main__.py +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/__main__.py @@ -1,16 +1,18 @@ from __future__ import annotations import argparse -import asyncio import os import signal import sys import threading -from typing import Any, Set -from .exceptions import ConnectionClosed -from .frames import Close -from .legacy.client import connect + +try: + import readline # noqa: F401 +except ImportError: # Windows has no `readline` normally + pass + +from .sync.client import ClientConnection, connect from .version import version as websockets_version @@ -46,21 +48,6 @@ if sys.platform == "win32": raise RuntimeError("unable to set console mode") -def exit_from_event_loop_thread( - loop: asyncio.AbstractEventLoop, - stop: asyncio.Future[None], -) -> None: - loop.stop() - if not stop.done(): - # When exiting the thread that runs the event loop, raise - # KeyboardInterrupt in the main thread to exit the program. - if sys.platform == "win32": - ctrl_c = signal.CTRL_C_EVENT - else: - ctrl_c = signal.SIGINT - os.kill(os.getpid(), ctrl_c) - - def print_during_input(string: str) -> None: sys.stdout.write( # Save cursor position @@ -93,63 +80,20 @@ def print_over_input(string: str) -> None: sys.stdout.flush() -async def run_client( - uri: str, - loop: asyncio.AbstractEventLoop, - inputs: asyncio.Queue[str], - stop: asyncio.Future[None], -) -> None: - try: - websocket = await connect(uri) - except Exception as exc: - print_over_input(f"Failed to connect to {uri}: {exc}.") - exit_from_event_loop_thread(loop, stop) - return - else: - print_during_input(f"Connected to {uri}.") - - try: - while True: - incoming: asyncio.Future[Any] = asyncio.create_task(websocket.recv()) - outgoing: asyncio.Future[Any] = asyncio.create_task(inputs.get()) - done: Set[asyncio.Future[Any]] - pending: Set[asyncio.Future[Any]] - done, pending = await asyncio.wait( - [incoming, outgoing, stop], return_when=asyncio.FIRST_COMPLETED - ) - - # Cancel pending tasks to avoid leaking them. - if incoming in pending: - incoming.cancel() - if outgoing in pending: - outgoing.cancel() - - if incoming in done: - try: - message = incoming.result() - except ConnectionClosed: - break - else: - if isinstance(message, str): - print_during_input("< " + message) - else: - print_during_input("< (binary) " + message.hex()) - - if outgoing in done: - message = outgoing.result() - await websocket.send(message) - - if stop in done: - break - - finally: - await websocket.close() - assert websocket.close_code is not None and websocket.close_reason is not None - close_status = Close(websocket.close_code, websocket.close_reason) - - print_over_input(f"Connection closed: {close_status}.") - - exit_from_event_loop_thread(loop, stop) +def print_incoming_messages(websocket: ClientConnection, stop: threading.Event) -> None: + for message in websocket: + if isinstance(message, str): + print_during_input("< " + message) + else: + print_during_input("< (binary) " + message.hex()) + if not stop.is_set(): + # When the server closes the connection, raise KeyboardInterrupt + # in the main thread to exit the program. + if sys.platform == "win32": + ctrl_c = signal.CTRL_C_EVENT + else: + ctrl_c = signal.SIGINT + os.kill(os.getpid(), ctrl_c) def main() -> None: @@ -184,29 +128,17 @@ def main() -> None: sys.stderr.flush() try: - import readline # noqa - except ImportError: # Windows has no `readline` normally - pass - - # Create an event loop that will run in a background thread. - loop = asyncio.new_event_loop() - - # Due to zealous removal of the loop parameter in the Queue constructor, - # we need a factory coroutine to run in the freshly created event loop. - async def queue_factory() -> asyncio.Queue[str]: - return asyncio.Queue() - - # Create a queue of user inputs. There's no need to limit its size. - inputs: asyncio.Queue[str] = loop.run_until_complete(queue_factory()) - - # Create a stop condition when receiving SIGINT or SIGTERM. - stop: asyncio.Future[None] = loop.create_future() + websocket = connect(args.uri) + except Exception as exc: + print(f"Failed to connect to {args.uri}: {exc}.") + sys.exit(1) + else: + print(f"Connected to {args.uri}.") - # Schedule the task that will manage the connection. - loop.create_task(run_client(args.uri, loop, inputs, stop)) + stop = threading.Event() - # Start the event loop in a background thread. - thread = threading.Thread(target=loop.run_forever) + # Start the thread that reads messages from the connection. + thread = threading.Thread(target=print_incoming_messages, args=(websocket, stop)) thread.start() # Read from stdin in the main thread in order to receive signals. @@ -214,17 +146,14 @@ def main() -> None: while True: # Since there's no size limit, put_nowait is identical to put. message = input("> ") - loop.call_soon_threadsafe(inputs.put_nowait, message) + websocket.send(message) except (KeyboardInterrupt, EOFError): # ^C, ^D - loop.call_soon_threadsafe(stop.set_result, None) + stop.set() + websocket.close() + print_over_input("Connection closed.") - # Wait for the event loop to terminate. thread.join() - # For reasons unclear, even though the loop is closed in the thread, - # it still thinks it's running here. - loop.close() - if __name__ == "__main__": main() diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/auth.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/auth.py index afcb38cffe2..b792e02f5cc 100644 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets/auth.py +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/auth.py @@ -1,4 +1,6 @@ from __future__ import annotations # See #940 for why lazy_import isn't used here for backwards compatibility. -from .legacy.auth import * # noqa +# See #1400 for why listing compatibility imports in __all__ helps PyCharm. +from .legacy.auth import * +from .legacy.auth import __all__ # noqa: F401 diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/client.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/client.py index df8e53429ab..b2f622042df 100644 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets/client.py +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/client.py @@ -1,8 +1,8 @@ from __future__ import annotations -from typing import Generator, List, Optional, Sequence +import warnings +from typing import Any, Generator, List, Optional, Sequence -from .connection import CLIENT, CONNECTING, OPEN, Connection, State from .datastructures import Headers, MultipleValuesError from .exceptions import ( InvalidHandshake, @@ -23,8 +23,8 @@ from .headers import ( parse_subprotocol, parse_upgrade, ) -from .http import USER_AGENT from .http11 import Request, Response +from .protocol import CLIENT, CONNECTING, OPEN, Protocol, State from .typing import ( ConnectionOption, ExtensionHeader, @@ -38,13 +38,15 @@ from .utils import accept_key, generate_key # See #940 for why lazy_import isn't used here for backwards compatibility. -from .legacy.client import * # isort:skip # noqa +# See #1400 for why listing compatibility imports in __all__ helps PyCharm. +from .legacy.client import * # isort:skip # noqa: I001 +from .legacy.client import __all__ as legacy__all__ -__all__ = ["ClientConnection"] +__all__ = ["ClientProtocol"] + legacy__all__ -class ClientConnection(Connection): +class ClientProtocol(Protocol): """ Sans-I/O implementation of a WebSocket client connection. @@ -60,16 +62,17 @@ class ClientConnection(Connection): preference. state: initial state of the WebSocket connection. max_size: maximum size of incoming messages in bytes; - :obj:`None` to disable the limit. + :obj:`None` disables the limit. logger: logger for this connection; defaults to ``logging.getLogger("websockets.client")``; - see the :doc:`logging guide <../topics/logging>` for details. + see the :doc:`logging guide <../../topics/logging>` for details. """ def __init__( self, wsuri: WebSocketURI, + *, origin: Optional[Origin] = None, extensions: Optional[Sequence[ClientExtensionFactory]] = None, subprotocols: Optional[Sequence[Subprotocol]] = None, @@ -89,7 +92,7 @@ class ClientConnection(Connection): self.available_subprotocols = subprotocols self.key = generate_key() - def connect(self) -> Request: # noqa: F811 + def connect(self) -> Request: """ Create a handshake request to open a connection. @@ -131,8 +134,6 @@ class ClientConnection(Connection): protocol_header = build_subprotocol(self.available_subprotocols) headers["Sec-WebSocket-Protocol"] = protocol_header - headers["User-Agent"] = USER_AGENT - return Request(self.wsuri.resource_name, headers) def process_response(self, response: Response) -> None: @@ -223,7 +224,6 @@ class ClientConnection(Connection): extensions = headers.get_all("Sec-WebSocket-Extensions") if extensions: - if self.available_extensions is None: raise InvalidHandshake("no extensions supported") @@ -232,9 +232,7 @@ class ClientConnection(Connection): ) for name, response_params in parsed_extensions: - for extension_factory in self.available_extensions: - # Skip non-matching extensions based on their name. if extension_factory.name != name: continue @@ -281,7 +279,6 @@ class ClientConnection(Connection): subprotocols = headers.get_all("Sec-WebSocket-Protocol") if subprotocols: - if self.available_subprotocols is None: raise InvalidHandshake("no subprotocols supported") @@ -317,11 +314,17 @@ class ClientConnection(Connection): def parse(self) -> Generator[None, None, None]: if self.state is CONNECTING: - response = yield from Response.parse( - self.reader.read_line, - self.reader.read_exact, - self.reader.read_to_eof, - ) + try: + response = yield from Response.parse( + self.reader.read_line, + self.reader.read_exact, + self.reader.read_to_eof, + ) + except Exception as exc: + self.handshake_exc = exc + self.parser = self.discard() + next(self.parser) # start coroutine + yield if self.debug: code, phrase = response.status_code, response.reason_phrase @@ -335,13 +338,23 @@ class ClientConnection(Connection): self.process_response(response) except InvalidHandshake as exc: response._exception = exc + self.events.append(response) self.handshake_exc = exc self.parser = self.discard() next(self.parser) # start coroutine - else: - assert self.state is CONNECTING - self.state = OPEN - finally: - self.events.append(response) + yield + + assert self.state is CONNECTING + self.state = OPEN + self.events.append(response) yield from super().parse() + + +class ClientConnection(ClientProtocol): + def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn( + "ClientConnection was renamed to ClientProtocol", + DeprecationWarning, + ) + super().__init__(*args, **kwargs) diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/connection.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/connection.py index db8b5369935..88bcda1aafe 100644 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets/connection.py +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/connection.py @@ -1,702 +1,13 @@ from __future__ import annotations -import enum -import logging -import uuid -from typing import Generator, List, Optional, Type, Union +import warnings -from .exceptions import ( - ConnectionClosed, - ConnectionClosedError, - ConnectionClosedOK, - InvalidState, - PayloadTooBig, - ProtocolError, -) -from .extensions import Extension -from .frames import ( - OK_CLOSE_CODES, - OP_BINARY, - OP_CLOSE, - OP_CONT, - OP_PING, - OP_PONG, - OP_TEXT, - Close, - Frame, -) -from .http11 import Request, Response -from .streams import StreamReader -from .typing import LoggerLike, Origin, Subprotocol - - -__all__ = [ - "Connection", - "Side", - "State", - "SEND_EOF", -] - -Event = Union[Request, Response, Frame] -"""Events that :meth:`~Connection.events_received` may return.""" - - -class Side(enum.IntEnum): - """A WebSocket connection is either a server or a client.""" - - SERVER, CLIENT = range(2) - - -SERVER = Side.SERVER -CLIENT = Side.CLIENT - - -class State(enum.IntEnum): - """A WebSocket connection is in one of these four states.""" - - CONNECTING, OPEN, CLOSING, CLOSED = range(4) - - -CONNECTING = State.CONNECTING -OPEN = State.OPEN -CLOSING = State.CLOSING -CLOSED = State.CLOSED - - -SEND_EOF = b"" -"""Sentinel signaling that the TCP connection must be half-closed.""" - - -class Connection: - """ - Sans-I/O implementation of a WebSocket connection. - - Args: - side: :attr:`~Side.CLIENT` or :attr:`~Side.SERVER`. - state: initial state of the WebSocket connection. - max_size: maximum size of incoming messages in bytes; - :obj:`None` to disable the limit. - logger: logger for this connection; depending on ``side``, - defaults to ``logging.getLogger("websockets.client")`` - or ``logging.getLogger("websockets.server")``; - see the :doc:`logging guide <../topics/logging>` for details. - - """ - - def __init__( - self, - side: Side, - state: State = OPEN, - max_size: Optional[int] = 2**20, - logger: Optional[LoggerLike] = None, - ) -> None: - # Unique identifier. For logs. - self.id: uuid.UUID = uuid.uuid4() - """Unique identifier of the connection. Useful in logs.""" - - # Logger or LoggerAdapter for this connection. - if logger is None: - logger = logging.getLogger(f"websockets.{side.name.lower()}") - self.logger: LoggerLike = logger - """Logger for this connection.""" - - # Track if DEBUG is enabled. Shortcut logging calls if it isn't. - self.debug = logger.isEnabledFor(logging.DEBUG) - - # Connection side. CLIENT or SERVER. - self.side = side - - # Connection state. Initially OPEN because subclasses handle CONNECTING. - self.state = state - - # Maximum size of incoming messages in bytes. - self.max_size = max_size - - # Current size of incoming message in bytes. Only set while reading a - # fragmented message i.e. a data frames with the FIN bit not set. - self.cur_size: Optional[int] = None - - # True while sending a fragmented message i.e. a data frames with the - # FIN bit not set. - self.expect_continuation_frame = False - - # WebSocket protocol parameters. - self.origin: Optional[Origin] = None - self.extensions: List[Extension] = [] - self.subprotocol: Optional[Subprotocol] = None - - # Close code and reason, set when a close frame is sent or received. - self.close_rcvd: Optional[Close] = None - self.close_sent: Optional[Close] = None - self.close_rcvd_then_sent: Optional[bool] = None - - # Track if an exception happened during the handshake. - self.handshake_exc: Optional[Exception] = None - """ - Exception to raise if the opening handshake failed. - - :obj:`None` if the opening handshake succeeded. - - """ - - # Track if send_eof() was called. - self.eof_sent = False - - # Parser state. - self.reader = StreamReader() - self.events: List[Event] = [] - self.writes: List[bytes] = [] - self.parser = self.parse() - next(self.parser) # start coroutine - self.parser_exc: Optional[Exception] = None - - @property - def state(self) -> State: - """ - WebSocket connection state. - - Defined in 4.1, 4.2, 7.1.3, and 7.1.4 of :rfc:`6455`. - - """ - return self._state - - @state.setter - def state(self, state: State) -> None: - if self.debug: - self.logger.debug("= connection is %s", state.name) - self._state = state - - @property - def close_code(self) -> Optional[int]: - """ - `WebSocket close code`_. - - .. _WebSocket close code: - https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.5 - - :obj:`None` if the connection isn't closed yet. - - """ - if self.state is not CLOSED: - return None - elif self.close_rcvd is None: - return 1006 - else: - return self.close_rcvd.code - - @property - def close_reason(self) -> Optional[str]: - """ - `WebSocket close reason`_. - - .. _WebSocket close reason: - https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.6 - - :obj:`None` if the connection isn't closed yet. - - """ - if self.state is not CLOSED: - return None - elif self.close_rcvd is None: - return "" - else: - return self.close_rcvd.reason - - @property - def close_exc(self) -> ConnectionClosed: - """ - Exception to raise when trying to interact with a closed connection. - - Don't raise this exception while the connection :attr:`state` - is :attr:`~websockets.connection.State.CLOSING`; wait until - it's :attr:`~websockets.connection.State.CLOSED`. - - Indeed, the exception includes the close code and reason, which are - known only once the connection is closed. - - Raises: - AssertionError: if the connection isn't closed yet. - - """ - assert self.state is CLOSED, "connection isn't closed yet" - exc_type: Type[ConnectionClosed] - if ( - self.close_rcvd is not None - and self.close_sent is not None - and self.close_rcvd.code in OK_CLOSE_CODES - and self.close_sent.code in OK_CLOSE_CODES - ): - exc_type = ConnectionClosedOK - else: - exc_type = ConnectionClosedError - exc: ConnectionClosed = exc_type( - self.close_rcvd, - self.close_sent, - self.close_rcvd_then_sent, - ) - # Chain to the exception raised in the parser, if any. - exc.__cause__ = self.parser_exc - return exc - - # Public methods for receiving data. - - def receive_data(self, data: bytes) -> None: - """ - Receive data from the network. - - After calling this method: - - - You must call :meth:`data_to_send` and send this data to the network. - - You should call :meth:`events_received` and process resulting events. - - Raises: - EOFError: if :meth:`receive_eof` was called earlier. - - """ - self.reader.feed_data(data) - next(self.parser) - - def receive_eof(self) -> None: - """ - Receive the end of the data stream from the network. - - After calling this method: - - - You must call :meth:`data_to_send` and send this data to the network. - - You aren't expected to call :meth:`events_received`; it won't return - any new events. - - Raises: - EOFError: if :meth:`receive_eof` was called earlier. - - """ - self.reader.feed_eof() - next(self.parser) - - # Public methods for sending events. - - def send_continuation(self, data: bytes, fin: bool) -> None: - """ - Send a `Continuation frame`_. - - .. _Continuation frame: - https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - Parameters: - data: payload containing the same kind of data - as the initial frame. - fin: FIN bit; set it to :obj:`True` if this is the last frame - of a fragmented message and to :obj:`False` otherwise. - - Raises: - ProtocolError: if a fragmented message isn't in progress. - - """ - if not self.expect_continuation_frame: - raise ProtocolError("unexpected continuation frame") - self.expect_continuation_frame = not fin - self.send_frame(Frame(OP_CONT, data, fin)) - - def send_text(self, data: bytes, fin: bool = True) -> None: - """ - Send a `Text frame`_. - - .. _Text frame: - https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - - Parameters: - data: payload containing text encoded with UTF-8. - fin: FIN bit; set it to :obj:`False` if this is the first frame of - a fragmented message. - - Raises: - ProtocolError: if a fragmented message is in progress. - - """ - if self.expect_continuation_frame: - raise ProtocolError("expected a continuation frame") - self.expect_continuation_frame = not fin - self.send_frame(Frame(OP_TEXT, data, fin)) - - def send_binary(self, data: bytes, fin: bool = True) -> None: - """ - Send a `Binary frame`_. +# lazy_import doesn't support this use case. +from .protocol import SEND_EOF, Protocol as Connection, Side, State # noqa: F401 - .. _Binary frame: - https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 - Parameters: - data: payload containing arbitrary binary data. - fin: FIN bit; set it to :obj:`False` if this is the first frame of - a fragmented message. - - Raises: - ProtocolError: if a fragmented message is in progress. - - """ - if self.expect_continuation_frame: - raise ProtocolError("expected a continuation frame") - self.expect_continuation_frame = not fin - self.send_frame(Frame(OP_BINARY, data, fin)) - - def send_close(self, code: Optional[int] = None, reason: str = "") -> None: - """ - Send a `Close frame`_. - - .. _Close frame: - https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.1 - - Parameters: - code: close code. - reason: close reason. - - Raises: - ProtocolError: if a fragmented message is being sent, if the code - isn't valid, or if a reason is provided without a code - - """ - if self.expect_continuation_frame: - raise ProtocolError("expected a continuation frame") - if code is None: - if reason != "": - raise ProtocolError("cannot send a reason without a code") - close = Close(1005, "") - data = b"" - else: - close = Close(code, reason) - data = close.serialize() - # send_frame() guarantees that self.state is OPEN at this point. - # 7.1.3. The WebSocket Closing Handshake is Started - self.send_frame(Frame(OP_CLOSE, data)) - self.close_sent = close - self.state = CLOSING - - def send_ping(self, data: bytes) -> None: - """ - Send a `Ping frame`_. - - .. _Ping frame: - https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2 - - Parameters: - data: payload containing arbitrary binary data. - - """ - self.send_frame(Frame(OP_PING, data)) - - def send_pong(self, data: bytes) -> None: - """ - Send a `Pong frame`_. - - .. _Pong frame: - https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.3 - - Parameters: - data: payload containing arbitrary binary data. - - """ - self.send_frame(Frame(OP_PONG, data)) - - def fail(self, code: int, reason: str = "") -> None: - """ - `Fail the WebSocket connection`_. - - .. _Fail the WebSocket connection: - https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.7 - - Parameters: - code: close code - reason: close reason - - Raises: - ProtocolError: if the code isn't valid. - """ - # 7.1.7. Fail the WebSocket Connection - - # Send a close frame when the state is OPEN (a close frame was already - # sent if it's CLOSING), except when failing the connection because - # of an error reading from or writing to the network. - if self.state is OPEN: - if code != 1006: - close = Close(code, reason) - data = close.serialize() - self.send_frame(Frame(OP_CLOSE, data)) - self.close_sent = close - self.state = CLOSING - - # When failing the connection, a server closes the TCP connection - # without waiting for the client to complete the handshake, while a - # client waits for the server to close the TCP connection, possibly - # after sending a close frame that the client will ignore. - if self.side is SERVER and not self.eof_sent: - self.send_eof() - - # 7.1.7. Fail the WebSocket Connection "An endpoint MUST NOT continue - # to attempt to process data(including a responding Close frame) from - # the remote endpoint after being instructed to _Fail the WebSocket - # Connection_." - self.parser = self.discard() - next(self.parser) # start coroutine - - # Public method for getting incoming events after receiving data. - - def events_received(self) -> List[Event]: - """ - Fetch events generated from data received from the network. - - Call this method immediately after any of the ``receive_*()`` methods. - - Process resulting events, likely by passing them to the application. - - Returns: - List[Event]: Events read from the connection. - """ - events, self.events = self.events, [] - return events - - # Public method for getting outgoing data after receiving data or sending events. - - def data_to_send(self) -> List[bytes]: - """ - Obtain data to send to the network. - - Call this method immediately after any of the ``receive_*()``, - ``send_*()``, or :meth:`fail` methods. - - Write resulting data to the connection. - - The empty bytestring :data:`~websockets.connection.SEND_EOF` signals - the end of the data stream. When you receive it, half-close the TCP - connection. - - Returns: - List[bytes]: Data to write to the connection. - - """ - writes, self.writes = self.writes, [] - return writes - - def close_expected(self) -> bool: - """ - Tell if the TCP connection is expected to close soon. - - Call this method immediately after any of the ``receive_*()`` or - :meth:`fail` methods. - - If it returns :obj:`True`, schedule closing the TCP connection after a - short timeout if the other side hasn't already closed it. - - Returns: - bool: Whether the TCP connection is expected to close soon. - - """ - # We expect a TCP close if and only if we sent a close frame: - # * Normal closure: once we send a close frame, we expect a TCP close: - # server waits for client to complete the TCP closing handshake; - # client waits for server to initiate the TCP closing handshake. - # * Abnormal closure: we always send a close frame and the same logic - # applies, except on EOFError where we don't send a close frame - # because we already received the TCP close, so we don't expect it. - # We already got a TCP Close if and only if the state is CLOSED. - return self.state is CLOSING or self.handshake_exc is not None - - # Private methods for receiving data. - - def parse(self) -> Generator[None, None, None]: - """ - Parse incoming data into frames. - - :meth:`receive_data` and :meth:`receive_eof` run this generator - coroutine until it needs more data or reaches EOF. - - """ - try: - while True: - if (yield from self.reader.at_eof()): - if self.debug: - self.logger.debug("< EOF") - # If the WebSocket connection is closed cleanly, with a - # closing handhshake, recv_frame() substitutes parse() - # with discard(). This branch is reached only when the - # connection isn't closed cleanly. - raise EOFError("unexpected end of stream") - - if self.max_size is None: - max_size = None - elif self.cur_size is None: - max_size = self.max_size - else: - max_size = self.max_size - self.cur_size - - # During a normal closure, execution ends here on the next - # iteration of the loop after receiving a close frame. At - # this point, recv_frame() replaced parse() by discard(). - frame = yield from Frame.parse( - self.reader.read_exact, - mask=self.side is SERVER, - max_size=max_size, - extensions=self.extensions, - ) - - if self.debug: - self.logger.debug("< %s", frame) - - self.recv_frame(frame) - - except ProtocolError as exc: - self.fail(1002, str(exc)) - self.parser_exc = exc - - except EOFError as exc: - self.fail(1006, str(exc)) - self.parser_exc = exc - - except UnicodeDecodeError as exc: - self.fail(1007, f"{exc.reason} at position {exc.start}") - self.parser_exc = exc - - except PayloadTooBig as exc: - self.fail(1009, str(exc)) - self.parser_exc = exc - - except Exception as exc: - self.logger.error("parser failed", exc_info=True) - # Don't include exception details, which may be security-sensitive. - self.fail(1011) - self.parser_exc = exc - - # During an abnormal closure, execution ends here after catching an - # exception. At this point, fail() replaced parse() by discard(). - yield - raise AssertionError("parse() shouldn't step after error") # pragma: no cover - - def discard(self) -> Generator[None, None, None]: - """ - Discard incoming data. - - This coroutine replaces :meth:`parse`: - - - after receiving a close frame, during a normal closure (1.4); - - after sending a close frame, during an abnormal closure (7.1.7). - - """ - # The server close the TCP connection in the same circumstances where - # discard() replaces parse(). The client closes the connection later, - # after the server closes the connection or a timeout elapses. - # (The latter case cannot be handled in this Sans-I/O layer.) - assert (self.side is SERVER) == (self.eof_sent) - while not (yield from self.reader.at_eof()): - self.reader.discard() - if self.debug: - self.logger.debug("< EOF") - # A server closes the TCP connection immediately, while a client - # waits for the server to close the TCP connection. - if self.side is CLIENT: - self.send_eof() - self.state = CLOSED - # If discard() completes normally, execution ends here. - yield - # Once the reader reaches EOF, its feed_data/eof() methods raise an - # error, so our receive_data/eof() methods don't step the generator. - raise AssertionError("discard() shouldn't step after EOF") # pragma: no cover - - def recv_frame(self, frame: Frame) -> None: - """ - Process an incoming frame. - - """ - if frame.opcode is OP_TEXT or frame.opcode is OP_BINARY: - if self.cur_size is not None: - raise ProtocolError("expected a continuation frame") - if frame.fin: - self.cur_size = None - else: - self.cur_size = len(frame.data) - - elif frame.opcode is OP_CONT: - if self.cur_size is None: - raise ProtocolError("unexpected continuation frame") - if frame.fin: - self.cur_size = None - else: - self.cur_size += len(frame.data) - - elif frame.opcode is OP_PING: - # 5.5.2. Ping: "Upon receipt of a Ping frame, an endpoint MUST - # send a Pong frame in response" - pong_frame = Frame(OP_PONG, frame.data) - self.send_frame(pong_frame) - - elif frame.opcode is OP_PONG: - # 5.5.3 Pong: "A response to an unsolicited Pong frame is not - # expected." - pass - - elif frame.opcode is OP_CLOSE: - # 7.1.5. The WebSocket Connection Close Code - # 7.1.6. The WebSocket Connection Close Reason - self.close_rcvd = Close.parse(frame.data) - if self.state is CLOSING: - assert self.close_sent is not None - self.close_rcvd_then_sent = False - - if self.cur_size is not None: - raise ProtocolError("incomplete fragmented message") - - # 5.5.1 Close: "If an endpoint receives a Close frame and did - # not previously send a Close frame, the endpoint MUST send a - # Close frame in response. (When sending a Close frame in - # response, the endpoint typically echos the status code it - # received.)" - - if self.state is OPEN: - # Echo the original data instead of re-serializing it with - # Close.serialize() because that fails when the close frame - # is empty and Close.parse() synthetizes a 1005 close code. - # The rest is identical to send_close(). - self.send_frame(Frame(OP_CLOSE, frame.data)) - self.close_sent = self.close_rcvd - self.close_rcvd_then_sent = True - self.state = CLOSING - - # 7.1.2. Start the WebSocket Closing Handshake: "Once an - # endpoint has both sent and received a Close control frame, - # that endpoint SHOULD _Close the WebSocket Connection_" - - # A server closes the TCP connection immediately, while a client - # waits for the server to close the TCP connection. - if self.side is SERVER: - self.send_eof() - - # 1.4. Closing Handshake: "after receiving a control frame - # indicating the connection should be closed, a peer discards - # any further data received." - self.parser = self.discard() - next(self.parser) # start coroutine - - else: # pragma: no cover - # This can't happen because Frame.parse() validates opcodes. - raise AssertionError(f"unexpected opcode: {frame.opcode:02x}") - - self.events.append(frame) - - # Private methods for sending events. - - def send_frame(self, frame: Frame) -> None: - if self.state is not OPEN: - raise InvalidState( - f"cannot write to a WebSocket in the {self.state.name} state" - ) - - if self.debug: - self.logger.debug("> %s", frame) - self.writes.append( - frame.serialize(mask=self.side is CLIENT, extensions=self.extensions) - ) - - def send_eof(self) -> None: - assert not self.eof_sent - self.eof_sent = True - if self.debug: - self.logger.debug("> EOF") - self.writes.append(SEND_EOF) +warnings.warn( + "websockets.connection was renamed to websockets.protocol " + "and Connection was renamed to Protocol", + DeprecationWarning, +) diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/datastructures.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/datastructures.py index 36a2cbaf99c..a0a648463ad 100644 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets/datastructures.py +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/datastructures.py @@ -1,6 +1,5 @@ from __future__ import annotations -import sys from typing import ( Any, Dict, @@ -9,17 +8,12 @@ from typing import ( List, Mapping, MutableMapping, + Protocol, Tuple, Union, ) -if sys.version_info[:2] >= (3, 8): - from typing import Protocol -else: # pragma: no cover - Protocol = object # mypy will report errors on Python 3.7. - - __all__ = ["Headers", "HeadersLike", "MultipleValuesError"] diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/exceptions.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/exceptions.py index 0c4fc51851a..f7169e3b178 100644 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets/exceptions.py +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/exceptions.py @@ -34,6 +34,7 @@ import http from typing import Optional from . import datastructures, frames, http11 +from .typing import StatusLike __all__ = [ @@ -120,19 +121,23 @@ class ConnectionClosed(WebSocketException): @property def code(self) -> int: - return 1006 if self.rcvd is None else self.rcvd.code + if self.rcvd is None: + return frames.CloseCode.ABNORMAL_CLOSURE + return self.rcvd.code @property def reason(self) -> str: - return "" if self.rcvd is None else self.rcvd.reason + if self.rcvd is None: + return "" + return self.rcvd.reason class ConnectionClosedError(ConnectionClosed): """ Like :exc:`ConnectionClosed`, when the connection terminated with an error. - A close code other than 1000 (OK) or 1001 (going away) was received or - sent, or the closing handshake didn't complete properly. + A close frame with a code other than 1000 (OK) or 1001 (going away) was + received or sent, or the closing handshake didn't complete properly. """ @@ -141,7 +146,8 @@ class ConnectionClosedOK(ConnectionClosed): """ Like :exc:`ConnectionClosed`, when the connection terminated properly. - A close code 1000 (OK) or 1001 (going away) was received and sent. + A close code with code 1000 (OK) or 1001 (going away) or without a code was + received and sent. """ @@ -171,7 +177,7 @@ class InvalidMessage(InvalidHandshake): class InvalidHeader(InvalidHandshake): """ - Raised when a HTTP header doesn't have a valid format or value. + Raised when an HTTP header doesn't have a valid format or value. """ @@ -190,7 +196,7 @@ class InvalidHeader(InvalidHandshake): class InvalidHeaderFormat(InvalidHeader): """ - Raised when a HTTP header cannot be parsed. + Raised when an HTTP header cannot be parsed. The format of the header doesn't match the grammar for that header. @@ -202,7 +208,7 @@ class InvalidHeaderFormat(InvalidHeader): class InvalidHeaderValue(InvalidHeader): """ - Raised when a HTTP header has a wrong value. + Raised when an HTTP header has a wrong value. The format of the header is correct but a value isn't acceptable. @@ -310,7 +316,7 @@ class InvalidParameterValue(NegotiationError): class AbortHandshake(InvalidHandshake): """ - Raised to abort the handshake on purpose and return a HTTP response. + Raised to abort the handshake on purpose and return an HTTP response. This exception is an implementation detail. @@ -325,11 +331,12 @@ class AbortHandshake(InvalidHandshake): def __init__( self, - status: http.HTTPStatus, + status: StatusLike, headers: datastructures.HeadersLike, body: bytes = b"", ) -> None: - self.status = status + # If a user passes an int instead of a HTTPStatus, fix it automatically. + self.status = http.HTTPStatus(status) self.headers = datastructures.Headers(headers) self.body = body @@ -369,7 +376,7 @@ class InvalidState(WebSocketException, AssertionError): class InvalidURI(WebSocketException): """ - Raised when connecting to an URI that isn't a valid WebSocket URI. + Raised when connecting to a URI that isn't a valid WebSocket URI. """ diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/extensions/base.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/extensions/base.py index 06096761857..6c481a46cc7 100644 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets/extensions/base.py +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/extensions/base.py @@ -38,6 +38,7 @@ class Extension: PayloadTooBig: if decoding the payload exceeds ``max_size``. """ + raise NotImplementedError def encode(self, frame: frames.Frame) -> frames.Frame: """ @@ -50,6 +51,7 @@ class Extension: Frame: Encoded frame. """ + raise NotImplementedError class ClientExtensionFactory: @@ -69,6 +71,7 @@ class ClientExtensionFactory: List[ExtensionParameter]: Parameters to send to the server. """ + raise NotImplementedError def process_response_params( self, @@ -91,6 +94,7 @@ class ClientExtensionFactory: NegotiationError: if parameters aren't acceptable. """ + raise NotImplementedError class ServerExtensionFactory: @@ -126,3 +130,4 @@ class ServerExtensionFactory: the client aren't acceptable. """ + raise NotImplementedError diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/extensions/permessage_deflate.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/extensions/permessage_deflate.py index e0de5e8f855..b391837c666 100644 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets/extensions/permessage_deflate.py +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/extensions/permessage_deflate.py @@ -211,7 +211,6 @@ def _extract_parameters( client_max_window_bits: Optional[Union[int, bool]] = None for name, value in params: - if name == "server_no_context_takeover": if server_no_context_takeover: raise exceptions.DuplicateParameter(name) diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/frames.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/frames.py index 043b688b522..6b1befb2e03 100644 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets/frames.py +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/frames.py @@ -13,7 +13,7 @@ from .typing import Data try: from .speedups import apply_mask -except ImportError: # pragma: no cover +except ImportError: from .utils import apply_mask @@ -52,45 +52,70 @@ DATA_OPCODES = OP_CONT, OP_TEXT, OP_BINARY CTRL_OPCODES = OP_CLOSE, OP_PING, OP_PONG -# See https://www.iana.org/assignments/websocket/websocket.xhtml -CLOSE_CODES = { - 1000: "OK", - 1001: "going away", - 1002: "protocol error", - 1003: "unsupported type", +class CloseCode(enum.IntEnum): + """Close code values for WebSocket close frames.""" + + NORMAL_CLOSURE = 1000 + GOING_AWAY = 1001 + PROTOCOL_ERROR = 1002 + UNSUPPORTED_DATA = 1003 # 1004 is reserved - 1005: "no status code [internal]", - 1006: "connection closed abnormally [internal]", - 1007: "invalid data", - 1008: "policy violation", - 1009: "message too big", - 1010: "extension required", - 1011: "unexpected error", - 1012: "service restart", - 1013: "try again later", - 1014: "bad gateway", - 1015: "TLS failure [internal]", + NO_STATUS_RCVD = 1005 + ABNORMAL_CLOSURE = 1006 + INVALID_DATA = 1007 + POLICY_VIOLATION = 1008 + MESSAGE_TOO_BIG = 1009 + MANDATORY_EXTENSION = 1010 + INTERNAL_ERROR = 1011 + SERVICE_RESTART = 1012 + TRY_AGAIN_LATER = 1013 + BAD_GATEWAY = 1014 + TLS_HANDSHAKE = 1015 + + +# See https://www.iana.org/assignments/websocket/websocket.xhtml +CLOSE_CODE_EXPLANATIONS: dict[int, str] = { + CloseCode.NORMAL_CLOSURE: "OK", + CloseCode.GOING_AWAY: "going away", + CloseCode.PROTOCOL_ERROR: "protocol error", + CloseCode.UNSUPPORTED_DATA: "unsupported data", + CloseCode.NO_STATUS_RCVD: "no status received [internal]", + CloseCode.ABNORMAL_CLOSURE: "abnormal closure [internal]", + CloseCode.INVALID_DATA: "invalid frame payload data", + CloseCode.POLICY_VIOLATION: "policy violation", + CloseCode.MESSAGE_TOO_BIG: "message too big", + CloseCode.MANDATORY_EXTENSION: "mandatory extension", + CloseCode.INTERNAL_ERROR: "internal error", + CloseCode.SERVICE_RESTART: "service restart", + CloseCode.TRY_AGAIN_LATER: "try again later", + CloseCode.BAD_GATEWAY: "bad gateway", + CloseCode.TLS_HANDSHAKE: "TLS handshake failure [internal]", } # Close code that are allowed in a close frame. # Using a set optimizes `code in EXTERNAL_CLOSE_CODES`. EXTERNAL_CLOSE_CODES = { - 1000, - 1001, - 1002, - 1003, - 1007, - 1008, - 1009, - 1010, - 1011, - 1012, - 1013, - 1014, + CloseCode.NORMAL_CLOSURE, + CloseCode.GOING_AWAY, + CloseCode.PROTOCOL_ERROR, + CloseCode.UNSUPPORTED_DATA, + CloseCode.INVALID_DATA, + CloseCode.POLICY_VIOLATION, + CloseCode.MESSAGE_TOO_BIG, + CloseCode.MANDATORY_EXTENSION, + CloseCode.INTERNAL_ERROR, + CloseCode.SERVICE_RESTART, + CloseCode.TRY_AGAIN_LATER, + CloseCode.BAD_GATEWAY, } -OK_CLOSE_CODES = {1000, 1001} + +OK_CLOSE_CODES = { + CloseCode.NORMAL_CLOSURE, + CloseCode.GOING_AWAY, + CloseCode.NO_STATUS_RCVD, +} BytesLike = bytes, bytearray, memoryview @@ -123,7 +148,7 @@ class Frame: def __str__(self) -> str: """ - Return a human-readable represention of a frame. + Return a human-readable representation of a frame. """ coding = None @@ -191,6 +216,8 @@ class Frame: extensions: list of extensions, applied in reverse order. Raises: + EOFError: if the connection is closed without a full WebSocket frame. + UnicodeDecodeError: if the frame contains invalid UTF-8. PayloadTooBig: if the frame's payload size exceeds ``max_size``. ProtocolError: if the frame contains incorrect values. @@ -383,7 +410,7 @@ class Close: def __str__(self) -> str: """ - Return a human-readable represention of a close code and reason. + Return a human-readable representation of a close code and reason. """ if 3000 <= self.code < 4000: @@ -391,7 +418,7 @@ class Close: elif 4000 <= self.code < 5000: explanation = "private use" else: - explanation = CLOSE_CODES.get(self.code, "unknown") + explanation = CLOSE_CODE_EXPLANATIONS.get(self.code, "unknown") result = f"{self.code} ({explanation})" if self.reason: @@ -419,7 +446,7 @@ class Close: close.check() return close elif len(data) == 0: - return cls(1005, "") + return cls(CloseCode.NO_STATUS_RCVD, "") else: raise exceptions.ProtocolError("close frame too short") diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/http.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/http.py index b14fa94bdce..9f86f6a1ffa 100644 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets/http.py +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/http.py @@ -1,6 +1,7 @@ from __future__ import annotations import sys +import typing from .imports import lazy_import from .version import version as websockets_version @@ -9,18 +10,22 @@ from .version import version as websockets_version # For backwards compatibility: -lazy_import( - globals(), - # Headers and MultipleValuesError used to be defined in this module. - aliases={ - "Headers": ".datastructures", - "MultipleValuesError": ".datastructures", - }, - deprecated_aliases={ - "read_request": ".legacy.http", - "read_response": ".legacy.http", - }, -) +# When type checking, import non-deprecated aliases eagerly. Else, import on demand. +if typing.TYPE_CHECKING: + from .datastructures import Headers, MultipleValuesError # noqa: F401 +else: + lazy_import( + globals(), + # Headers and MultipleValuesError used to be defined in this module. + aliases={ + "Headers": ".datastructures", + "MultipleValuesError": ".datastructures", + }, + deprecated_aliases={ + "read_request": ".legacy.http", + "read_response": ".legacy.http", + }, + ) __all__ = ["USER_AGENT"] diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/http11.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/http11.py index 84048fa47b3..ec4e3b8b7d5 100644 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets/http11.py +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/http11.py @@ -8,14 +8,12 @@ from typing import Callable, Generator, Optional from . import datastructures, exceptions -# Maximum total size of headers is around 256 * 4 KiB = 1 MiB -MAX_HEADERS = 256 +# Maximum total size of headers is around 128 * 8 KiB = 1 MiB. +MAX_HEADERS = 128 -# We can use the same limit for the request line and header lines: -# "GET <4096 bytes> HTTP/1.1\r\n" = 4111 bytes -# "Set-Cookie: <4097 bytes>\r\n" = 4111 bytes -# (RFC requires 4096 bytes; for some reason Firefox supports 4097 bytes.) -MAX_LINE = 4111 +# Limit request line and header lines. 8KiB is the most common default +# configuration of popular HTTP servers. +MAX_LINE = 8192 # Support for HTTP response bodies is intended to read an error message # returned by a server. It isn't designed to perform large file transfers. @@ -70,7 +68,7 @@ class Request: def exception(self) -> Optional[Exception]: # pragma: no cover warnings.warn( "Request.exception is deprecated; " - "use ServerConnection.handshake_exc instead", + "use ServerProtocol.handshake_exc instead", DeprecationWarning, ) return self._exception @@ -174,7 +172,7 @@ class Response: def exception(self) -> Optional[Exception]: # pragma: no cover warnings.warn( "Response.exception is deprecated; " - "use ClientConnection.handshake_exc instead", + "use ClientProtocol.handshake_exc instead", DeprecationWarning, ) return self._exception diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/async_timeout.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/async_timeout.py new file mode 100644 index 00000000000..8264094f5b2 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/async_timeout.py @@ -0,0 +1,265 @@ +# From https://github.com/aio-libs/async-timeout/blob/master/async_timeout/__init__.py +# Licensed under the Apache License (Apache-2.0) + +import asyncio +import enum +import sys +import warnings +from types import TracebackType +from typing import Optional, Type + + +# From https://github.com/python/typing_extensions/blob/main/src/typing_extensions.py +# Licensed under the Python Software Foundation License (PSF-2.0) + +if sys.version_info >= (3, 11): + from typing import final +else: + # @final exists in 3.8+, but we backport it for all versions + # before 3.11 to keep support for the __final__ attribute. + # See https://bugs.python.org/issue46342 + def final(f): + """This decorator can be used to indicate to type checkers that + the decorated method cannot be overridden, and decorated class + cannot be subclassed. For example: + + class Base: + @final + def done(self) -> None: + ... + class Sub(Base): + def done(self) -> None: # Error reported by type checker + ... + @final + class Leaf: + ... + class Other(Leaf): # Error reported by type checker + ... + + There is no runtime checking of these properties. The decorator + sets the ``__final__`` attribute to ``True`` on the decorated object + to allow runtime introspection. + """ + try: + f.__final__ = True + except (AttributeError, TypeError): + # Skip the attribute silently if it is not writable. + # AttributeError happens if the object has __slots__ or a + # read-only property, TypeError if it's a builtin class. + pass + return f + + +# End https://github.com/aio-libs/async-timeout/blob/master/async_timeout/__init__.py + +__version__ = "4.0.2" + + +__all__ = ("timeout", "timeout_at", "Timeout") + + +def timeout(delay: Optional[float]) -> "Timeout": + """timeout context manager. + + Useful in cases when you want to apply timeout logic around block + of code or in cases when asyncio.wait_for is not suitable. For example: + + >>> async with timeout(0.001): + ... async with aiohttp.get('https://github.com') as r: + ... await r.text() + + + delay - value in seconds or None to disable timeout logic + """ + loop = asyncio.get_running_loop() + if delay is not None: + deadline = loop.time() + delay # type: Optional[float] + else: + deadline = None + return Timeout(deadline, loop) + + +def timeout_at(deadline: Optional[float]) -> "Timeout": + """Schedule the timeout at absolute time. + + deadline argument points on the time in the same clock system + as loop.time(). + + Please note: it is not POSIX time but a time with + undefined starting base, e.g. the time of the system power on. + + >>> async with timeout_at(loop.time() + 10): + ... async with aiohttp.get('https://github.com') as r: + ... await r.text() + + + """ + loop = asyncio.get_running_loop() + return Timeout(deadline, loop) + + +class _State(enum.Enum): + INIT = "INIT" + ENTER = "ENTER" + TIMEOUT = "TIMEOUT" + EXIT = "EXIT" + + +@final +class Timeout: + # Internal class, please don't instantiate it directly + # Use timeout() and timeout_at() public factories instead. + # + # Implementation note: `async with timeout()` is preferred + # over `with timeout()`. + # While technically the Timeout class implementation + # doesn't need to be async at all, + # the `async with` statement explicitly points that + # the context manager should be used from async function context. + # + # This design allows to avoid many silly misusages. + # + # TimeoutError is raised immediately when scheduled + # if the deadline is passed. + # The purpose is to time out as soon as possible + # without waiting for the next await expression. + + __slots__ = ("_deadline", "_loop", "_state", "_timeout_handler") + + def __init__( + self, deadline: Optional[float], loop: asyncio.AbstractEventLoop + ) -> None: + self._loop = loop + self._state = _State.INIT + + self._timeout_handler = None # type: Optional[asyncio.Handle] + if deadline is None: + self._deadline = None # type: Optional[float] + else: + self.update(deadline) + + def __enter__(self) -> "Timeout": + warnings.warn( + "with timeout() is deprecated, use async with timeout() instead", + DeprecationWarning, + stacklevel=2, + ) + self._do_enter() + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> Optional[bool]: + self._do_exit(exc_type) + return None + + async def __aenter__(self) -> "Timeout": + self._do_enter() + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> Optional[bool]: + self._do_exit(exc_type) + return None + + @property + def expired(self) -> bool: + """Is timeout expired during execution?""" + return self._state == _State.TIMEOUT + + @property + def deadline(self) -> Optional[float]: + return self._deadline + + def reject(self) -> None: + """Reject scheduled timeout if any.""" + # cancel is maybe better name but + # task.cancel() raises CancelledError in asyncio world. + if self._state not in (_State.INIT, _State.ENTER): + raise RuntimeError(f"invalid state {self._state.value}") + self._reject() + + def _reject(self) -> None: + if self._timeout_handler is not None: + self._timeout_handler.cancel() + self._timeout_handler = None + + def shift(self, delay: float) -> None: + """Advance timeout on delay seconds. + + The delay can be negative. + + Raise RuntimeError if shift is called when deadline is not scheduled + """ + deadline = self._deadline + if deadline is None: + raise RuntimeError("cannot shift timeout if deadline is not scheduled") + self.update(deadline + delay) + + def update(self, deadline: float) -> None: + """Set deadline to absolute value. + + deadline argument points on the time in the same clock system + as loop.time(). + + If new deadline is in the past the timeout is raised immediately. + + Please note: it is not POSIX time but a time with + undefined starting base, e.g. the time of the system power on. + """ + if self._state == _State.EXIT: + raise RuntimeError("cannot reschedule after exit from context manager") + if self._state == _State.TIMEOUT: + raise RuntimeError("cannot reschedule expired timeout") + if self._timeout_handler is not None: + self._timeout_handler.cancel() + self._deadline = deadline + if self._state != _State.INIT: + self._reschedule() + + def _reschedule(self) -> None: + assert self._state == _State.ENTER + deadline = self._deadline + if deadline is None: + return + + now = self._loop.time() + if self._timeout_handler is not None: + self._timeout_handler.cancel() + + task = asyncio.current_task() + if deadline <= now: + self._timeout_handler = self._loop.call_soon(self._on_timeout, task) + else: + self._timeout_handler = self._loop.call_at(deadline, self._on_timeout, task) + + def _do_enter(self) -> None: + if self._state != _State.INIT: + raise RuntimeError(f"invalid state {self._state.value}") + self._state = _State.ENTER + self._reschedule() + + def _do_exit(self, exc_type: Optional[Type[BaseException]]) -> None: + if exc_type is asyncio.CancelledError and self._state == _State.TIMEOUT: + self._timeout_handler = None + raise asyncio.TimeoutError + # timeout has not expired + self._state = _State.EXIT + self._reject() + return None + + def _on_timeout(self, task: "asyncio.Task[None]") -> None: + task.cancel() + self._state = _State.TIMEOUT + # drop the reference early + self._timeout_handler = None + + +# End https://github.com/aio-libs/async-timeout/blob/master/async_timeout/__init__.py diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/auth.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/auth.py index 8825c14ecf7..d3425836e18 100644 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/auth.py +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/auth.py @@ -67,7 +67,7 @@ class BasicAuthWebSocketServerProtocol(WebSocketServerProtocol): Returns: bool: :obj:`True` if the handshake should continue; - :obj:`False` if it should fail with a HTTP 401 error. + :obj:`False` if it should fail with an HTTP 401 error. """ if self._check_credentials is not None: @@ -81,7 +81,7 @@ class BasicAuthWebSocketServerProtocol(WebSocketServerProtocol): request_headers: Headers, ) -> Optional[HTTPResponse]: """ - Check HTTP Basic Auth and return a HTTP 401 response if needed. + Check HTTP Basic Auth and return an HTTP 401 response if needed. """ try: @@ -118,8 +118,8 @@ def basic_auth_protocol_factory( realm: Optional[str] = None, credentials: Optional[Union[Credentials, Iterable[Credentials]]] = None, check_credentials: Optional[Callable[[str, str], Awaitable[bool]]] = None, - create_protocol: Optional[Callable[[Any], BasicAuthWebSocketServerProtocol]] = None, -) -> Callable[[Any], BasicAuthWebSocketServerProtocol]: + create_protocol: Optional[Callable[..., BasicAuthWebSocketServerProtocol]] = None, +) -> Callable[..., BasicAuthWebSocketServerProtocol]: """ Protocol factory that enforces HTTP Basic Auth. @@ -135,20 +135,20 @@ def basic_auth_protocol_factory( ) Args: - realm: indicates the scope of protection. It should contain only ASCII - characters because the encoding of non-ASCII characters is - undefined. Refer to section 2.2 of :rfc:`7235` for details. - credentials: defines hard coded authorized credentials. It can be a + realm: Scope of protection. It should contain only ASCII characters + because the encoding of non-ASCII characters is undefined. + Refer to section 2.2 of :rfc:`7235` for details. + credentials: Hard coded authorized credentials. It can be a ``(username, password)`` pair or a list of such pairs. - check_credentials: defines a coroutine that verifies credentials. - This coroutine receives ``username`` and ``password`` arguments + check_credentials: Coroutine that verifies credentials. + It receives ``username`` and ``password`` arguments and returns a :class:`bool`. One of ``credentials`` or ``check_credentials`` must be provided but not both. - create_protocol: factory that creates the protocol. By default, this + create_protocol: Factory that creates the protocol. By default, this is :class:`BasicAuthWebSocketServerProtocol`. It can be replaced by a subclass. Raises: - TypeError: if the ``credentials`` or ``check_credentials`` argument is + TypeError: If the ``credentials`` or ``check_credentials`` argument is wrong. """ @@ -175,11 +175,7 @@ def basic_auth_protocol_factory( return hmac.compare_digest(expected_password, password) if create_protocol is None: - # Not sure why mypy cannot figure this out. - create_protocol = cast( - Callable[[Any], BasicAuthWebSocketServerProtocol], - BasicAuthWebSocketServerProtocol, - ) + create_protocol = BasicAuthWebSocketServerProtocol return functools.partial( create_protocol, diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/client.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/client.py index fadc3efe876..48622523ee1 100644 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/client.py +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/client.py @@ -44,6 +44,7 @@ from ..headers import ( from ..http import USER_AGENT from ..typing import ExtensionHeader, LoggerLike, Origin, Subprotocol from ..uri import WebSocketURI, parse_uri +from .compatibility import asyncio_timeout from .handshake import build_request, check_response from .http import read_response from .protocol import WebSocketCommonProtocol @@ -65,12 +66,13 @@ class WebSocketClientProtocol(WebSocketCommonProtocol): await process(message) The iterator exits normally when the connection is closed with close code - 1000 (OK) or 1001 (going away). It raises + 1000 (OK) or 1001 (going away) or without a close code. It raises a :exc:`~websockets.exceptions.ConnectionClosedError` when the connection is closed with any other code. See :func:`connect` for the documentation of ``logger``, ``origin``, - ``extensions``, ``subprotocols``, and ``extra_headers``. + ``extensions``, ``subprotocols``, ``extra_headers``, and + ``user_agent_header``. See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``, @@ -89,6 +91,7 @@ class WebSocketClientProtocol(WebSocketCommonProtocol): extensions: Optional[Sequence[ClientExtensionFactory]] = None, subprotocols: Optional[Sequence[Subprotocol]] = None, extra_headers: Optional[HeadersLike] = None, + user_agent_header: Optional[str] = USER_AGENT, **kwargs: Any, ) -> None: if logger is None: @@ -98,6 +101,7 @@ class WebSocketClientProtocol(WebSocketCommonProtocol): self.available_extensions = extensions self.available_subprotocols = subprotocols self.extra_headers = extra_headers + self.user_agent_header = user_agent_header def write_http_request(self, path: str, headers: Headers) -> None: """ @@ -127,16 +131,12 @@ class WebSocketClientProtocol(WebSocketCommonProtocol): after this coroutine returns. Raises: - InvalidMessage: if the HTTP message is malformed or isn't an + InvalidMessage: If the HTTP message is malformed or isn't an HTTP/1.1 GET response. """ try: status_code, reason, headers = await read_response(self.reader) - # Remove this branch when dropping support for Python < 3.8 - # because CancelledError no longer inherits Exception. - except asyncio.CancelledError: # pragma: no cover - raise except Exception as exc: raise InvalidMessage("did not receive a valid HTTP response") from exc @@ -185,7 +185,6 @@ class WebSocketClientProtocol(WebSocketCommonProtocol): header_values = headers.get_all("Sec-WebSocket-Extensions") if header_values: - if available_extensions is None: raise InvalidHandshake("no extensions supported") @@ -194,9 +193,7 @@ class WebSocketClientProtocol(WebSocketCommonProtocol): ) for name, response_params in parsed_header_values: - for extension_factory in available_extensions: - # Skip non-matching extensions based on their name. if extension_factory.name != name: continue @@ -242,7 +239,6 @@ class WebSocketClientProtocol(WebSocketCommonProtocol): header_values = headers.get_all("Sec-WebSocket-Protocol") if header_values: - if available_subprotocols is None: raise InvalidHandshake("no subprotocols supported") @@ -274,15 +270,15 @@ class WebSocketClientProtocol(WebSocketCommonProtocol): Args: wsuri: URI of the WebSocket server. - origin: value of the ``Origin`` header. - available_extensions: list of supported extensions, in order in - which they should be tried. - available_subprotocols: list of supported subprotocols, in order - of decreasing preference. - extra_headers: arbitrary HTTP headers to add to the request. + origin: Value of the ``Origin`` header. + extensions: List of supported extensions, in order in which they + should be negotiated and run. + subprotocols: List of supported subprotocols, in order of decreasing + preference. + extra_headers: Arbitrary HTTP headers to add to the handshake request. Raises: - InvalidHandshake: if the handshake fails. + InvalidHandshake: If the handshake fails. """ request_headers = Headers() @@ -315,7 +311,8 @@ class WebSocketClientProtocol(WebSocketCommonProtocol): if self.extra_headers is not None: request_headers.update(self.extra_headers) - request_headers.setdefault("User-Agent", USER_AGENT) + if self.user_agent_header is not None: + request_headers.setdefault("User-Agent", self.user_agent_header) self.write_http_request(wsuri.resource_name, request_headers) @@ -376,25 +373,26 @@ class Connect: Args: uri: URI of the WebSocket server. - create_protocol: factory for the :class:`asyncio.Protocol` managing - the connection; defaults to :class:`WebSocketClientProtocol`; may - be set to a wrapper or a subclass to customize connection handling. - logger: logger for this connection; - defaults to ``logging.getLogger("websockets.client")``; - see the :doc:`logging guide <../topics/logging>` for details. - compression: shortcut that enables the "permessage-deflate" extension - by default; may be set to :obj:`None` to disable compression; - see the :doc:`compression guide <../topics/compression>` for details. - origin: value of the ``Origin`` header. This is useful when connecting - to a server that validates the ``Origin`` header to defend against - Cross-Site WebSocket Hijacking attacks. - extensions: list of supported extensions, in order in which they - should be tried. - subprotocols: list of supported subprotocols, in order of decreasing + create_protocol: Factory for the :class:`asyncio.Protocol` managing + the connection. It defaults to :class:`WebSocketClientProtocol`. + Set it to a wrapper or a subclass to customize connection handling. + logger: Logger for this client. + It defaults to ``logging.getLogger("websockets.client")``. + See the :doc:`logging guide <../../topics/logging>` for details. + compression: The "permessage-deflate" extension is enabled by default. + Set ``compression`` to :obj:`None` to disable it. See the + :doc:`compression guide <../../topics/compression>` for details. + origin: Value of the ``Origin`` header, for servers that require it. + extensions: List of supported extensions, in order in which they + should be negotiated and run. + subprotocols: List of supported subprotocols, in order of decreasing preference. - extra_headers: arbitrary HTTP headers to add to the request. - open_timeout: timeout for opening the connection in seconds; - :obj:`None` to disable the timeout + extra_headers: Arbitrary HTTP headers to add to the handshake request. + user_agent_header: Value of the ``User-Agent`` request header. + It defaults to ``"Python/x.y.z websockets/X.Y"``. + Setting it to :obj:`None` removes the header. + open_timeout: Timeout for opening the connection in seconds. + :obj:`None` disables the timeout. See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``, @@ -415,13 +413,11 @@ class Connect: the TCP connection. The host name from ``uri`` is still used in the TLS handshake for secure connections and in the ``Host`` header. - Returns: - WebSocketClientProtocol: WebSocket connection. - Raises: - InvalidURI: if ``uri`` isn't a valid WebSocket URI. - InvalidHandshake: if the opening handshake fails. - ~asyncio.TimeoutError: if the opening handshake times out. + InvalidURI: If ``uri`` isn't a valid WebSocket URI. + OSError: If the TCP connection fails. + InvalidHandshake: If the opening handshake fails. + ~asyncio.TimeoutError: If the opening handshake times out. """ @@ -431,13 +427,14 @@ class Connect: self, uri: str, *, - create_protocol: Optional[Callable[[Any], WebSocketClientProtocol]] = None, + create_protocol: Optional[Callable[..., WebSocketClientProtocol]] = None, logger: Optional[LoggerLike] = None, compression: Optional[str] = "deflate", origin: Optional[Origin] = None, extensions: Optional[Sequence[ClientExtensionFactory]] = None, subprotocols: Optional[Sequence[Subprotocol]] = None, extra_headers: Optional[HeadersLike] = None, + user_agent_header: Optional[str] = USER_AGENT, open_timeout: Optional[float] = 10, ping_interval: Optional[float] = 20, ping_timeout: Optional[float] = 20, @@ -503,6 +500,7 @@ class Connect: extensions=extensions, subprotocols=subprotocols, extra_headers=extra_headers, + user_agent_header=user_agent_header, ping_interval=ping_interval, ping_timeout=ping_timeout, close_timeout=close_timeout, @@ -530,6 +528,8 @@ class Connect: else: # If sock is given, host and port shouldn't be specified. host, port = None, None + if kwargs.get("ssl"): + kwargs.setdefault("server_hostname", wsuri.host) # If host and port are given, override values from the URI. host = kwargs.pop("host", host) port = kwargs.pop("port", port) @@ -597,10 +597,6 @@ class Connect: try: async with self as protocol: yield protocol - # Remove this branch when dropping support for Python < 3.8 - # because CancelledError no longer inherits Exception. - except asyncio.CancelledError: # pragma: no cover - raise except Exception: # Add a random initial delay between 0 and 5 seconds. # See 7.2.3. Recovering from Abnormal Closure in RFC 6544. @@ -647,13 +643,13 @@ class Connect: return self.__await_impl_timeout__().__await__() async def __await_impl_timeout__(self) -> WebSocketClientProtocol: - return await asyncio.wait_for(self.__await_impl__(), self.open_timeout) + async with asyncio_timeout(self.open_timeout): + return await self.__await_impl__() async def __await_impl__(self) -> WebSocketClientProtocol: for redirects in range(self.MAX_REDIRECTS_ALLOWED): - transport, protocol = await self._create_connection() - protocol = cast(WebSocketClientProtocol, protocol) - + _transport, _protocol = await self._create_connection() + protocol = cast(WebSocketClientProtocol, _protocol) try: await protocol.handshake( self._wsuri, @@ -701,7 +697,7 @@ def unix_connect( It's mainly useful for debugging servers listening on Unix sockets. Args: - path: file system path to the Unix socket. + path: File system path to the Unix socket. uri: URI of the WebSocket server; the host is used in the TLS handshake for secure connections and in the ``Host`` header. diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/compatibility.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/compatibility.py index df81de9dbce..6bd01e70dee 100644 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/compatibility.py +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/compatibility.py @@ -1,13 +1,12 @@ from __future__ import annotations -import asyncio import sys -from typing import Any, Dict -def loop_if_py_lt_38(loop: asyncio.AbstractEventLoop) -> Dict[str, Any]: - """ - Helper for the removal of the loop argument in Python 3.10. +__all__ = ["asyncio_timeout"] - """ - return {"loop": loop} if sys.version_info[:2] < (3, 8) else {} + +if sys.version_info[:2] >= (3, 11): + from asyncio import timeout as asyncio_timeout # noqa: F401 +else: + from .async_timeout import timeout as asyncio_timeout # noqa: F401 diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/framing.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/framing.py index c4de7eb28bc..b77b869e3fa 100644 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/framing.py +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/framing.py @@ -1,6 +1,5 @@ from __future__ import annotations -import dataclasses import struct from typing import Any, Awaitable, Callable, NamedTuple, Optional, Sequence, Tuple @@ -10,12 +9,11 @@ from ..exceptions import PayloadTooBig, ProtocolError try: from ..speedups import apply_mask -except ImportError: # pragma: no cover +except ImportError: from ..utils import apply_mask class Frame(NamedTuple): - fin: bool opcode: frames.Opcode data: bytes @@ -53,16 +51,16 @@ class Frame(NamedTuple): Read a WebSocket frame. Args: - reader: coroutine that reads exactly the requested number of + reader: Coroutine that reads exactly the requested number of bytes, unless the end of file is reached. - mask: whether the frame should be masked i.e. whether the read + mask: Whether the frame should be masked i.e. whether the read happens on the server side. - max_size: maximum payload size in bytes. - extensions: list of extensions, applied in reverse order. + max_size: Maximum payload size in bytes. + extensions: List of extensions, applied in reverse order. Raises: - PayloadTooBig: if the frame exceeds ``max_size``. - ProtocolError: if the frame contains incorrect values. + PayloadTooBig: If the frame exceeds ``max_size``. + ProtocolError: If the frame contains incorrect values. """ @@ -130,14 +128,14 @@ class Frame(NamedTuple): Write a WebSocket frame. Args: - frame: frame to write. - write: function that writes bytes. - mask: whether the frame should be masked i.e. whether the write + frame: Frame to write. + write: Function that writes bytes. + mask: Whether the frame should be masked i.e. whether the write happens on the client side. - extensions: list of extensions, applied in order. + extensions: List of extensions, applied in order. Raises: - ProtocolError: if the frame contains incorrect values. + ProtocolError: If the frame contains incorrect values. """ # The frame is written in a single call to write in order to prevent @@ -147,8 +145,11 @@ class Frame(NamedTuple): # Backwards compatibility with previously documented public APIs - -from ..frames import Close, prepare_ctrl as encode_data, prepare_data # noqa +from ..frames import ( # noqa: E402, F401, I001 + Close, + prepare_ctrl as encode_data, + prepare_data, +) def parse_close(data: bytes) -> Tuple[int, str]: @@ -156,14 +157,15 @@ def parse_close(data: bytes) -> Tuple[int, str]: Parse the payload from a close frame. Returns: - Tuple[int, str]: close code and reason. + Close code and reason. Raises: - ProtocolError: if data is ill-formed. - UnicodeDecodeError: if the reason isn't valid UTF-8. + ProtocolError: If data is ill-formed. + UnicodeDecodeError: If the reason isn't valid UTF-8. """ - return dataclasses.astuple(Close.parse(data)) # type: ignore + close = Close.parse(data) + return close.code, close.reason def serialize_close(code: int, reason: str) -> bytes: diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/handshake.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/handshake.py index 569937bb9a9..ad8faf04045 100644 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/handshake.py +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/handshake.py @@ -21,7 +21,7 @@ def build_request(headers: Headers) -> str: Update request headers passed in argument. Args: - headers: handshake request headers. + headers: Handshake request headers. Returns: str: ``key`` that must be passed to :func:`check_response`. @@ -45,14 +45,14 @@ def check_request(headers: Headers) -> str: the responsibility of the caller. Args: - headers: handshake request headers. + headers: Handshake request headers. Returns: str: ``key`` that must be passed to :func:`build_response`. Raises: - InvalidHandshake: if the handshake request is invalid; - then the server must return 400 Bad Request error. + InvalidHandshake: If the handshake request is invalid. + Then, the server must return a 400 Bad Request error. """ connection: List[ConnectionOption] = sum( @@ -110,8 +110,8 @@ def build_response(headers: Headers, key: str) -> None: Update response headers passed in argument. Args: - headers: handshake response headers. - key: returned by :func:`check_request`. + headers: Handshake response headers. + key: Returned by :func:`check_request`. """ headers["Upgrade"] = "websocket" @@ -128,11 +128,11 @@ def check_response(headers: Headers, key: str) -> None: the caller. Args: - headers: handshake response headers. - key: returned by :func:`build_request`. + headers: Handshake response headers. + key: Returned by :func:`build_request`. Raises: - InvalidHandshake: if the handshake response is invalid. + InvalidHandshake: If the handshake response is invalid. """ connection: List[ConnectionOption] = sum( diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/http.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/http.py index cc2ef1f067d..2ac7f7092d5 100644 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/http.py +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/http.py @@ -10,8 +10,8 @@ from ..exceptions import SecurityError __all__ = ["read_request", "read_response"] -MAX_HEADERS = 256 -MAX_LINE = 4110 +MAX_HEADERS = 128 +MAX_LINE = 8192 def d(value: bytes) -> str: @@ -56,12 +56,12 @@ async def read_request(stream: asyncio.StreamReader) -> Tuple[str, Headers]: body, it may be read from ``stream`` after this coroutine returns. Args: - stream: input to read the request from + stream: Input to read the request from. Raises: - EOFError: if the connection is closed without a full HTTP request - SecurityError: if the request exceeds a security limit - ValueError: if the request isn't well formatted + EOFError: If the connection is closed without a full HTTP request. + SecurityError: If the request exceeds a security limit. + ValueError: If the request isn't well formatted. """ # https://www.rfc-editor.org/rfc/rfc7230.html#section-3.1.1 @@ -103,12 +103,12 @@ async def read_response(stream: asyncio.StreamReader) -> Tuple[int, str, Headers body, it may be read from ``stream`` after this coroutine returns. Args: - stream: input to read the response from + stream: Input to read the response from. Raises: - EOFError: if the connection is closed without a full HTTP response - SecurityError: if the response exceeds a security limit - ValueError: if the response isn't well formatted + EOFError: If the connection is closed without a full HTTP response. + SecurityError: If the response exceeds a security limit. + ValueError: If the response isn't well formatted. """ # https://www.rfc-editor.org/rfc/rfc7230.html#section-3.1.2 @@ -192,7 +192,7 @@ async def read_line(stream: asyncio.StreamReader) -> bytes: """ # Security: this is bounded by the StreamReader's limit (default = 32 KiB). line = await stream.readline() - # Security: this guarantees header values are small (hard-coded = 4 KiB) + # Security: this guarantees header values are small (hard-coded = 8 KiB) if len(line) > MAX_LINE: raise SecurityError("line too long") # Not mandatory but safe - https://www.rfc-editor.org/rfc/rfc7230.html#section-3.5 diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/protocol.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/protocol.py index 3f734fe7602..19cee0e652b 100644 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/protocol.py +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/protocol.py @@ -7,6 +7,8 @@ import logging import random import ssl import struct +import sys +import time import uuid import warnings from typing import ( @@ -14,17 +16,18 @@ from typing import ( AsyncIterable, AsyncIterator, Awaitable, + Callable, Deque, Dict, Iterable, List, Mapping, Optional, + Tuple, Union, cast, ) -from ..connection import State from ..datastructures import Headers from ..exceptions import ( ConnectionClosed, @@ -44,12 +47,14 @@ from ..frames import ( OP_PONG, OP_TEXT, Close, + CloseCode, Opcode, prepare_ctrl, prepare_data, ) +from ..protocol import State from ..typing import Data, LoggerLike, Subprotocol -from .compatibility import loop_if_py_lt_38 +from .compatibility import asyncio_timeout from .framing import Frame @@ -76,38 +81,38 @@ class WebSocketCommonProtocol(asyncio.Protocol): simplicity. Once the connection is open, a Ping_ frame is sent every ``ping_interval`` - seconds. This serves as a keepalive. It helps keeping the connection - open, especially in the presence of proxies with short timeouts on - inactive connections. Set ``ping_interval`` to :obj:`None` to disable - this behavior. + seconds. This serves as a keepalive. It helps keeping the connection open, + especially in the presence of proxies with short timeouts on inactive + connections. Set ``ping_interval`` to :obj:`None` to disable this behavior. .. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2 If the corresponding Pong_ frame isn't received within ``ping_timeout`` - seconds, the connection is considered unusable and is closed with code - 1011. This ensures that the remote endpoint remains responsive. Set + seconds, the connection is considered unusable and is closed with code 1011. + This ensures that the remote endpoint remains responsive. Set ``ping_timeout`` to :obj:`None` to disable this behavior. .. _Pong: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.3 + See the discussion of :doc:`timeouts <../../topics/timeouts>` for details. + The ``close_timeout`` parameter defines a maximum wait time for completing the closing handshake and terminating the TCP connection. For legacy reasons, :meth:`close` completes in at most ``5 * close_timeout`` seconds for clients and ``4 * close_timeout`` for servers. - See the discussion of :doc:`timeouts <../topics/timeouts>` for details. - - ``close_timeout`` needs to be a parameter of the protocol because - websockets usually calls :meth:`close` implicitly upon exit: + ``close_timeout`` is a parameter of the protocol because websockets usually + calls :meth:`close` implicitly upon exit: - * on the client side, when :func:`~websockets.client.connect` is used as a + * on the client side, when using :func:`~websockets.client.connect` as a context manager; - * on the server side, when the connection handler terminates; + * on the server side, when the connection handler terminates. - To apply a timeout to any other API, wrap it in :func:`~asyncio.wait_for`. + To apply a timeout to any other API, wrap it in :func:`~asyncio.timeout` or + :func:`~asyncio.wait_for`. The ``max_size`` parameter enforces the maximum size for incoming messages - in bytes. The default value is 1 MiB. If a larger message is received, + in bytes. The default value is 1 MiB. If a larger message is received, :meth:`recv` will raise :exc:`~websockets.exceptions.ConnectionClosedError` and the connection will be closed with code 1009. @@ -124,38 +129,38 @@ class WebSocketCommonProtocol(asyncio.Protocol): Since Python can use up to 4 bytes of memory to represent a single character, each connection may use up to ``4 * max_size * max_queue`` - bytes of memory to store incoming messages. By default, this is 128 MiB. + bytes of memory to store incoming messages. By default, this is 128 MiB. You may want to lower the limits, depending on your application's requirements. The ``read_limit`` argument sets the high-water limit of the buffer for incoming bytes. The low-water limit is half the high-water limit. The - default value is 64 KiB, half of asyncio's default (based on the current + default value is 64 KiB, half of asyncio's default (based on the current implementation of :class:`~asyncio.StreamReader`). The ``write_limit`` argument sets the high-water limit of the buffer for outgoing bytes. The low-water limit is a quarter of the high-water limit. - The default value is 64 KiB, equal to asyncio's default (based on the + The default value is 64 KiB, equal to asyncio's default (based on the current implementation of ``FlowControlMixin``). - See the discussion of :doc:`memory usage <../topics/memory>` for details. + See the discussion of :doc:`memory usage <../../topics/memory>` for details. Args: - logger: logger for this connection; - defaults to ``logging.getLogger("websockets.protocol")``; - see the :doc:`logging guide <../topics/logging>` for details. - ping_interval: delay between keepalive pings in seconds; - :obj:`None` to disable keepalive pings. - ping_timeout: timeout for keepalive pings in seconds; - :obj:`None` to disable timeouts. - close_timeout: timeout for closing the connection in seconds; - for legacy reasons, the actual timeout is 4 or 5 times larger. - max_size: maximum size of incoming messages in bytes; - :obj:`None` to disable the limit. - max_queue: maximum number of incoming messages in receive buffer; - :obj:`None` to disable the limit. - read_limit: high-water mark of read buffer in bytes. - write_limit: high-water mark of write buffer in bytes. + logger: Logger for this server. + It defaults to ``logging.getLogger("websockets.protocol")``. + See the :doc:`logging guide <../../topics/logging>` for details. + ping_interval: Delay between keepalive pings in seconds. + :obj:`None` disables keepalive pings. + ping_timeout: Timeout for keepalive pings in seconds. + :obj:`None` disables timeouts. + close_timeout: Timeout for closing the connection in seconds. + For legacy reasons, the actual timeout is 4 or 5 times larger. + max_size: Maximum size of incoming messages in bytes. + :obj:`None` disables the limit. + max_queue: Maximum number of incoming messages in receive buffer. + :obj:`None` disables the limit. + read_limit: High-water mark of read buffer in bytes. + write_limit: High-water mark of write buffer in bytes. """ @@ -217,8 +222,6 @@ class WebSocketCommonProtocol(asyncio.Protocol): # Logger or LoggerAdapter for this connection. if logger is None: logger = logging.getLogger("websockets.protocol") - # https://github.com/python/typeshed/issues/5561 - logger = cast(logging.Logger, logger) self.logger: LoggerLike = logging.LoggerAdapter(logger, {"websocket": self}) """Logger for this connection.""" @@ -242,7 +245,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): self._paused = False self._drain_waiter: Optional[asyncio.Future[None]] = None - self._drain_lock = asyncio.Lock(**loop_if_py_lt_38(loop)) + self._drain_lock = asyncio.Lock() # This class implements the data transfer and closing handshake, which # are shared between the client-side and the server-side. @@ -285,7 +288,19 @@ class WebSocketCommonProtocol(asyncio.Protocol): self._fragmented_message_waiter: Optional[asyncio.Future[None]] = None # Mapping of ping IDs to pong waiters, in chronological order. - self.pings: Dict[bytes, asyncio.Future[None]] = {} + self.pings: Dict[bytes, Tuple[asyncio.Future[float], float]] = {} + + self.latency: float = 0 + """ + Latency of the connection, in seconds. + + This value is updated after sending a ping frame and receiving a + matching pong frame. Before the first ping, :attr:`latency` is ``0``. + + By default, websockets enables a :ref:`keepalive <keepalive>` mechanism + that sends ping frames automatically at regular intervals. You can also + send ping frames and measure latency with :meth:`ping`. + """ # Task running the data transfer. self.transfer_data_task: asyncio.Task[None] @@ -325,7 +340,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): # write(...); yield from drain() # in a loop would never call connection_lost(), so it # would not see an error when the socket is closed. - await asyncio.sleep(0, **loop_if_py_lt_38(self.loop)) + await asyncio.sleep(0) await self._drain_helper() def connection_open(self) -> None: @@ -445,7 +460,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): if self.state is not State.CLOSED: return None elif self.close_rcvd is None: - return 1006 + return CloseCode.ABNORMAL_CLOSURE else: return self.close_rcvd.code @@ -471,10 +486,11 @@ class WebSocketCommonProtocol(asyncio.Protocol): """ Iterate on incoming messages. - The iterator exits normally when the connection is closed with the - close code 1000 (OK) or 1001(going away). It raises - a :exc:`~websockets.exceptions.ConnectionClosedError` exception when - the connection is closed with any other code. + The iterator exits normally when the connection is closed with the close + code 1000 (OK) or 1001 (going away) or without a close code. + + It raises a :exc:`~websockets.exceptions.ConnectionClosedError` + exception when the connection is closed with any other code. """ try: @@ -488,8 +504,8 @@ class WebSocketCommonProtocol(asyncio.Protocol): Receive the next message. When the connection is closed, :meth:`recv` raises - :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it - raises :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal + :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it raises + :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal connection closure and :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol error or a network failure. This is how you detect the end of the @@ -498,8 +514,8 @@ class WebSocketCommonProtocol(asyncio.Protocol): Canceling :meth:`recv` is safe. There's no risk of losing the next message. The next invocation of :meth:`recv` will return it. - This makes it possible to enforce a timeout by wrapping :meth:`recv` - in :func:`~asyncio.wait_for`. + This makes it possible to enforce a timeout by wrapping :meth:`recv` in + :func:`~asyncio.timeout` or :func:`~asyncio.wait_for`. Returns: Data: A string (:class:`str`) for a Text_ frame. A bytestring @@ -509,8 +525,8 @@ class WebSocketCommonProtocol(asyncio.Protocol): .. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 Raises: - ConnectionClosed: when the connection is closed. - RuntimeError: if two coroutines call :meth:`recv` concurrently. + ConnectionClosed: When the connection is closed. + RuntimeError: If two coroutines call :meth:`recv` concurrently. """ if self._pop_message_waiter is not None: @@ -536,7 +552,6 @@ class WebSocketCommonProtocol(asyncio.Protocol): await asyncio.wait( [pop_message_waiter, self.transfer_data_task], return_when=asyncio.FIRST_COMPLETED, - **loop_if_py_lt_38(self.loop), ) finally: self._pop_message_waiter = None @@ -613,8 +628,8 @@ class WebSocketCommonProtocol(asyncio.Protocol): to send. Raises: - ConnectionClosed: when the connection is closed. - TypeError: if ``message`` doesn't have a supported type. + ConnectionClosed: When the connection is closed. + TypeError: If ``message`` doesn't have a supported type. """ await self.ensure_open() @@ -639,16 +654,15 @@ class WebSocketCommonProtocol(asyncio.Protocol): # Fragmented message -- regular iterator. elif isinstance(message, Iterable): - # Work around https://github.com/python/mypy/issues/6227 message = cast(Iterable[Data], message) iter_message = iter(message) try: - message_chunk = next(iter_message) + fragment = next(iter_message) except StopIteration: return - opcode, data = prepare_data(message_chunk) + opcode, data = prepare_data(fragment) self._fragmented_message_waiter = asyncio.Future() try: @@ -656,8 +670,8 @@ class WebSocketCommonProtocol(asyncio.Protocol): await self.write_frame(False, opcode, data) # Other fragments. - for message_chunk in iter_message: - confirm_opcode, data = prepare_data(message_chunk) + for fragment in iter_message: + confirm_opcode, data = prepare_data(fragment) if confirm_opcode != opcode: raise TypeError("data contains inconsistent types") await self.write_frame(False, OP_CONT, data) @@ -668,7 +682,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): except (Exception, asyncio.CancelledError): # We're half-way through a fragmented message and we can't # complete it. This makes the connection unusable. - self.fail_connection(1011) + self.fail_connection(CloseCode.INTERNAL_ERROR) raise finally: @@ -678,18 +692,22 @@ class WebSocketCommonProtocol(asyncio.Protocol): # Fragmented message -- asynchronous iterator elif isinstance(message, AsyncIterable): - # aiter_message = aiter(message) without aiter - # https://github.com/python/mypy/issues/5738 - aiter_message = type(message).__aiter__(message) # type: ignore + # Implement aiter_message = aiter(message) without aiter + # Work around https://github.com/python/mypy/issues/5738 + aiter_message = cast( + Callable[[AsyncIterable[Data]], AsyncIterator[Data]], + type(message).__aiter__, + )(message) try: - # message_chunk = anext(aiter_message) without anext - # https://github.com/python/mypy/issues/5738 - message_chunk = await type(aiter_message).__anext__( # type: ignore - aiter_message - ) + # Implement fragment = anext(aiter_message) without anext + # Work around https://github.com/python/mypy/issues/5738 + fragment = await cast( + Callable[[AsyncIterator[Data]], Awaitable[Data]], + type(aiter_message).__anext__, + )(aiter_message) except StopAsyncIteration: return - opcode, data = prepare_data(message_chunk) + opcode, data = prepare_data(fragment) self._fragmented_message_waiter = asyncio.Future() try: @@ -697,11 +715,8 @@ class WebSocketCommonProtocol(asyncio.Protocol): await self.write_frame(False, opcode, data) # Other fragments. - # https://github.com/python/mypy/issues/5738 - # coverage reports this code as not covered, but it is - # exercised by tests - changing it breaks the tests! - async for message_chunk in aiter_message: # type: ignore # pragma: no cover # noqa - confirm_opcode, data = prepare_data(message_chunk) + async for fragment in aiter_message: + confirm_opcode, data = prepare_data(fragment) if confirm_opcode != opcode: raise TypeError("data contains inconsistent types") await self.write_frame(False, OP_CONT, data) @@ -712,7 +727,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): except (Exception, asyncio.CancelledError): # We're half-way through a fragmented message and we can't # complete it. This makes the connection unusable. - self.fail_connection(1011) + self.fail_connection(CloseCode.INTERNAL_ERROR) raise finally: @@ -722,7 +737,11 @@ class WebSocketCommonProtocol(asyncio.Protocol): else: raise TypeError("data must be str, bytes-like, or iterable") - async def close(self, code: int = 1000, reason: str = "") -> None: + async def close( + self, + code: int = CloseCode.NORMAL_CLOSURE, + reason: str = "", + ) -> None: """ Perform the closing handshake. @@ -747,19 +766,16 @@ class WebSocketCommonProtocol(asyncio.Protocol): """ try: - await asyncio.wait_for( - self.write_close_frame(Close(code, reason)), - self.close_timeout, - **loop_if_py_lt_38(self.loop), - ) + async with asyncio_timeout(self.close_timeout): + await self.write_close_frame(Close(code, reason)) except asyncio.TimeoutError: # If the close frame cannot be sent because the send buffers # are full, the closing handshake won't complete anyway. # Fail the connection to shut down faster. self.fail_connection() - # If no close frame is received within the timeout, wait_for() cancels - # the data transfer task and raises TimeoutError. + # If no close frame is received within the timeout, asyncio_timeout() + # cancels the data transfer task and raises TimeoutError. # If close() is called multiple times concurrently and one of these # calls hits the timeout, the data transfer task will be canceled. @@ -768,11 +784,8 @@ class WebSocketCommonProtocol(asyncio.Protocol): try: # If close() is canceled during the wait, self.transfer_data_task # is canceled before the timeout elapses. - await asyncio.wait_for( - self.transfer_data_task, - self.close_timeout, - **loop_if_py_lt_38(self.loop), - ) + async with asyncio_timeout(self.close_timeout): + await self.transfer_data_task except (asyncio.TimeoutError, asyncio.CancelledError): pass @@ -798,8 +811,8 @@ class WebSocketCommonProtocol(asyncio.Protocol): .. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2 - A ping may serve as a keepalive or as a check that the remote endpoint - received all messages up to this point + A ping may serve as a keepalive, as a check that the remote endpoint + received all messages up to this point, or to measure :attr:`latency`. Canceling :meth:`ping` is discouraged. If :meth:`ping` doesn't return immediately, it means the write buffer is full. If you don't want to @@ -814,18 +827,20 @@ class WebSocketCommonProtocol(asyncio.Protocol): containing four random bytes. Returns: - ~asyncio.Future: A future that will be completed when the - corresponding pong is received. You can ignore it if you - don't intend to wait. + ~asyncio.Future[float]: A future that will be completed when the + corresponding pong is received. You can ignore it if you don't + intend to wait. The result of the future is the latency of the + connection in seconds. :: pong_waiter = await ws.ping() - await pong_waiter # only if you want to wait for the pong + # only if you want to wait for the corresponding pong + latency = await pong_waiter Raises: - ConnectionClosed: when the connection is closed. - RuntimeError: if another ping was sent with the same data and + ConnectionClosed: When the connection is closed. + RuntimeError: If another ping was sent with the same data and the corresponding pong wasn't received yet. """ @@ -842,11 +857,14 @@ class WebSocketCommonProtocol(asyncio.Protocol): while data is None or data in self.pings: data = struct.pack("!I", random.getrandbits(32)) - self.pings[data] = self.loop.create_future() + pong_waiter = self.loop.create_future() + # Resolution of time.monotonic() may be too low on Windows. + ping_timestamp = time.perf_counter() + self.pings[data] = (pong_waiter, ping_timestamp) await self.write_frame(True, OP_PING, data) - return asyncio.shield(self.pings[data]) + return asyncio.shield(pong_waiter) async def pong(self, data: Data = b"") -> None: """ @@ -861,11 +879,11 @@ class WebSocketCommonProtocol(asyncio.Protocol): wait, you should close the connection. Args: - data (Data): payload of the pong; a string will be encoded to + data (Data): Payload of the pong. A string will be encoded to UTF-8. Raises: - ConnectionClosed: when the connection is closed. + ConnectionClosed: When the connection is closed. """ await self.ensure_open() @@ -973,7 +991,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): except ProtocolError as exc: self.transfer_data_exc = exc - self.fail_connection(1002) + self.fail_connection(CloseCode.PROTOCOL_ERROR) except (ConnectionError, TimeoutError, EOFError, ssl.SSLError) as exc: # Reading data with self.reader.readexactly may raise: @@ -984,15 +1002,15 @@ class WebSocketCommonProtocol(asyncio.Protocol): # bytes are available than requested; # - ssl.SSLError if the other side infringes the TLS protocol. self.transfer_data_exc = exc - self.fail_connection(1006) + self.fail_connection(CloseCode.ABNORMAL_CLOSURE) except UnicodeDecodeError as exc: self.transfer_data_exc = exc - self.fail_connection(1007) + self.fail_connection(CloseCode.INVALID_DATA) except PayloadTooBig as exc: self.transfer_data_exc = exc - self.fail_connection(1009) + self.fail_connection(CloseCode.MESSAGE_TOO_BIG) except Exception as exc: # This shouldn't happen often because exceptions expected under @@ -1001,7 +1019,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): self.logger.error("data transfer failed", exc_info=True) self.transfer_data_exc = exc - self.fail_connection(1011) + self.fail_connection(CloseCode.INTERNAL_ERROR) async def read_message(self) -> Optional[Data]: """ @@ -1030,7 +1048,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): return frame.data.decode("utf-8") if text else frame.data # 5.4. Fragmentation - chunks: List[Data] = [] + fragments: List[Data] = [] max_size = self.max_size if text: decoder_factory = codecs.getincrementaldecoder("utf-8") @@ -1038,14 +1056,14 @@ class WebSocketCommonProtocol(asyncio.Protocol): if max_size is None: def append(frame: Frame) -> None: - nonlocal chunks - chunks.append(decoder.decode(frame.data, frame.fin)) + nonlocal fragments + fragments.append(decoder.decode(frame.data, frame.fin)) else: def append(frame: Frame) -> None: - nonlocal chunks, max_size - chunks.append(decoder.decode(frame.data, frame.fin)) + nonlocal fragments, max_size + fragments.append(decoder.decode(frame.data, frame.fin)) assert isinstance(max_size, int) max_size -= len(frame.data) @@ -1053,14 +1071,14 @@ class WebSocketCommonProtocol(asyncio.Protocol): if max_size is None: def append(frame: Frame) -> None: - nonlocal chunks - chunks.append(frame.data) + nonlocal fragments + fragments.append(frame.data) else: def append(frame: Frame) -> None: - nonlocal chunks, max_size - chunks.append(frame.data) + nonlocal fragments, max_size + fragments.append(frame.data) assert isinstance(max_size, int) max_size -= len(frame.data) @@ -1074,7 +1092,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): raise ProtocolError("unexpected opcode") append(frame) - return ("" if text else b"").join(chunks) + return ("" if text else b"").join(fragments) async def read_data_frame(self, max_size: Optional[int]) -> Optional[Frame]: """ @@ -1099,7 +1117,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): try: # Echo the original data instead of re-serializing it with # Close.serialize() because that fails when the close frame - # is empty and Close.parse() synthetizes a 1005 close code. + # is empty and Close.parse() synthesizes a 1005 close code. await self.write_close_frame(self.close_rcvd, frame.data) except ConnectionClosed: # Connection closed before we could echo the close frame. @@ -1117,18 +1135,20 @@ class WebSocketCommonProtocol(asyncio.Protocol): elif frame.opcode == OP_PONG: if frame.data in self.pings: + pong_timestamp = time.perf_counter() # Sending a pong for only the most recent ping is legal. # Acknowledge all previous pings too in that case. ping_id = None ping_ids = [] - for ping_id, ping in self.pings.items(): + for ping_id, (pong_waiter, ping_timestamp) in self.pings.items(): ping_ids.append(ping_id) - if not ping.done(): - ping.set_result(None) + if not pong_waiter.done(): + pong_waiter.set_result(pong_timestamp - ping_timestamp) if ping_id == frame.data: + self.latency = pong_timestamp - ping_timestamp break - else: # pragma: no cover - assert False, "ping_id is in self.pings" + else: + raise AssertionError("solicited pong not found in pings") # Remove acknowledged pings from self.pings. for ping_id in ping_ids: del self.pings[ping_id] @@ -1231,10 +1251,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): try: while True: - await asyncio.sleep( - self.ping_interval, - **loop_if_py_lt_38(self.loop), - ) + await asyncio.sleep(self.ping_interval) # ping() raises CancelledError if the connection is closed, # when close_connection() cancels self.keepalive_ping_task. @@ -1247,23 +1264,18 @@ class WebSocketCommonProtocol(asyncio.Protocol): if self.ping_timeout is not None: try: - await asyncio.wait_for( - pong_waiter, - self.ping_timeout, - **loop_if_py_lt_38(self.loop), - ) + async with asyncio_timeout(self.ping_timeout): + await pong_waiter self.logger.debug("% received keepalive pong") except asyncio.TimeoutError: if self.debug: self.logger.debug("! timed out waiting for keepalive pong") - self.fail_connection(1011, "keepalive ping timeout") + self.fail_connection( + CloseCode.INTERNAL_ERROR, + "keepalive ping timeout", + ) break - # Remove this branch when dropping support for Python < 3.8 - # because CancelledError no longer inherits Exception. - except asyncio.CancelledError: - raise - except ConnectionClosed: pass @@ -1297,9 +1309,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): # A client should wait for a TCP close from the server. if self.is_client and hasattr(self, "transfer_data_task"): if await self.wait_for_connection_lost(): - # Coverage marks this line as a partially executed branch. - # I supect a bug in coverage. Ignore it for now. - return # pragma: no cover + return if self.debug: self.logger.debug("! timed out waiting for TCP close") @@ -1317,9 +1327,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): pass if await self.wait_for_connection_lost(): - # Coverage marks this line as a partially executed branch. - # I supect a bug in coverage. Ignore it for now. - return # pragma: no cover + return if self.debug: self.logger.debug("! timed out waiting for TCP close") @@ -1352,12 +1360,11 @@ class WebSocketCommonProtocol(asyncio.Protocol): # Abort the TCP connection. Buffers are discarded. if self.debug: self.logger.debug("x aborting TCP connection") - self.transport.abort() + # Due to a bug in coverage, this is erroneously reported as not covered. + self.transport.abort() # pragma: no cover # connection_lost() is called quickly after aborting. - # Coverage marks this line as a partially executed branch. - # I supect a bug in coverage. Ignore it for now. - await self.wait_for_connection_lost() # pragma: no cover + await self.wait_for_connection_lost() async def wait_for_connection_lost(self) -> bool: """ @@ -1369,11 +1376,8 @@ class WebSocketCommonProtocol(asyncio.Protocol): """ if not self.connection_lost_waiter.done(): try: - await asyncio.wait_for( - asyncio.shield(self.connection_lost_waiter), - self.close_timeout, - **loop_if_py_lt_38(self.loop), - ) + async with asyncio_timeout(self.close_timeout): + await asyncio.shield(self.connection_lost_waiter) except asyncio.TimeoutError: pass # Re-check self.connection_lost_waiter.done() synchronously because @@ -1381,7 +1385,11 @@ class WebSocketCommonProtocol(asyncio.Protocol): # and the moment this coroutine resumes running. return self.connection_lost_waiter.done() - def fail_connection(self, code: int = 1006, reason: str = "") -> None: + def fail_connection( + self, + code: int = CloseCode.ABNORMAL_CLOSURE, + reason: str = "", + ) -> None: """ 7.1.7. Fail the WebSocket Connection @@ -1412,7 +1420,7 @@ class WebSocketCommonProtocol(asyncio.Protocol): # sent if it's CLOSING), except when failing the connection because of # an error reading from or writing to the network. # Don't send a close frame if the connection is broken. - if code != 1006 and self.state is State.OPEN: + if code != CloseCode.ABNORMAL_CLOSURE and self.state is State.OPEN: close = Close(code, reason) # Write the close frame without draining the write buffer. @@ -1449,13 +1457,13 @@ class WebSocketCommonProtocol(asyncio.Protocol): assert self.state is State.CLOSED exc = self.connection_closed_exc() - for ping in self.pings.values(): - ping.set_exception(exc) + for pong_waiter, _ping_timestamp in self.pings.values(): + pong_waiter.set_exception(exc) # If the exception is never retrieved, it will be logged when ping # is garbage-collected. This is confusing for users. # Given that ping is done (with an exception), canceling it does # nothing, but it prevents logging the exception. - ping.cancel() + pong_waiter.cancel() # asyncio.Protocol methods @@ -1496,7 +1504,6 @@ class WebSocketCommonProtocol(asyncio.Protocol): self.connection_lost_waiter.set_result(None) if True: # pragma: no cover - # Copied from asyncio.StreamReaderProtocol if self.reader is not None: if exc is None: @@ -1552,13 +1559,17 @@ class WebSocketCommonProtocol(asyncio.Protocol): self.reader.feed_eof() -def broadcast(websockets: Iterable[WebSocketCommonProtocol], message: Data) -> None: +def broadcast( + websockets: Iterable[WebSocketCommonProtocol], + message: Data, + raise_exceptions: bool = False, +) -> None: """ Broadcast a message to several WebSocket connections. - A string (:class:`str`) is sent as a Text_ frame. A bytestring or - bytes-like object (:class:`bytes`, :class:`bytearray`, or - :class:`memoryview`) is sent as a Binary_ frame. + A string (:class:`str`) is sent as a Text_ frame. A bytestring or bytes-like + object (:class:`bytes`, :class:`bytearray`, or :class:`memoryview`) is sent + as a Binary_ frame. .. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 .. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 @@ -1566,33 +1577,42 @@ def broadcast(websockets: Iterable[WebSocketCommonProtocol], message: Data) -> N :func:`broadcast` pushes the message synchronously to all connections even if their write buffers are overflowing. There's no backpressure. - :func:`broadcast` skips silently connections that aren't open in order to - avoid errors on connections where the closing handshake is in progress. - - If you broadcast messages faster than a connection can handle them, - messages will pile up in its write buffer until the connection times out. - Keep low values for ``ping_interval`` and ``ping_timeout`` to prevent - excessive memory usage by slow connections when you use :func:`broadcast`. + If you broadcast messages faster than a connection can handle them, messages + will pile up in its write buffer until the connection times out. Keep + ``ping_interval`` and ``ping_timeout`` low to prevent excessive memory usage + from slow connections. Unlike :meth:`~websockets.server.WebSocketServerProtocol.send`, :func:`broadcast` doesn't support sending fragmented messages. Indeed, - fragmentation is useful for sending large messages without buffering - them in memory, while :func:`broadcast` buffers one copy per connection - as fast as possible. + fragmentation is useful for sending large messages without buffering them in + memory, while :func:`broadcast` buffers one copy per connection as fast as + possible. + + :func:`broadcast` skips connections that aren't open in order to avoid + errors on connections where the closing handshake is in progress. + + :func:`broadcast` ignores failures to write the message on some connections. + It continues writing to other connections. On Python 3.11 and above, you + may set ``raise_exceptions`` to :obj:`True` to record failures and raise all + exceptions in a :pep:`654` :exc:`ExceptionGroup`. Args: - websockets (Iterable[WebSocketCommonProtocol]): WebSocket connections - to which the message will be sent. - message (Data): message to send. + websockets: WebSocket connections to which the message will be sent. + message: Message to send. + raise_exceptions: Whether to raise an exception in case of failures. Raises: - RuntimeError: if a connection is busy sending a fragmented message. - TypeError: if ``message`` doesn't have a supported type. + TypeError: If ``message`` doesn't have a supported type. """ if not isinstance(message, (str, bytes, bytearray, memoryview)): raise TypeError("data must be str or bytes-like") + if raise_exceptions: + if sys.version_info[:2] < (3, 11): # pragma: no cover + raise ValueError("raise_exceptions requires at least Python 3.11") + exceptions = [] + opcode, data = prepare_data(message) for websocket in websockets: @@ -1600,6 +1620,26 @@ def broadcast(websockets: Iterable[WebSocketCommonProtocol], message: Data) -> N continue if websocket._fragmented_message_waiter is not None: - raise RuntimeError("busy sending a fragmented message") + if raise_exceptions: + exception = RuntimeError("sending a fragmented message") + exceptions.append(exception) + else: + websocket.logger.warning( + "skipped broadcast: sending a fragmented message", + ) + + try: + websocket.write_frame_sync(True, opcode, data) + except Exception as write_exception: + if raise_exceptions: + exception = RuntimeError("failed to write message") + exception.__cause__ = write_exception + exceptions.append(exception) + else: + websocket.logger.warning( + "skipped broadcast: failed to write message", + exc_info=True, + ) - websocket.write_frame_sync(True, opcode, data) + if raise_exceptions: + raise ExceptionGroup("skipped broadcast", exceptions) diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/server.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/server.py index 3e51db1b71e..7c24dd74af3 100644 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/server.py +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/legacy/server.py @@ -25,7 +25,6 @@ from typing import ( cast, ) -from ..connection import State from ..datastructures import Headers, HeadersLike, MultipleValuesError from ..exceptions import ( AbortHandshake, @@ -45,8 +44,9 @@ from ..headers import ( validate_subprotocols, ) from ..http import USER_AGENT -from ..typing import ExtensionHeader, LoggerLike, Origin, Subprotocol -from .compatibility import loop_if_py_lt_38 +from ..protocol import State +from ..typing import ExtensionHeader, LoggerLike, Origin, StatusLike, Subprotocol +from .compatibility import asyncio_timeout from .handshake import build_response, check_request from .http import read_request from .protocol import WebSocketCommonProtocol @@ -57,7 +57,7 @@ __all__ = ["serve", "unix_serve", "WebSocketServerProtocol", "WebSocketServer"] HeadersLikeOrCallable = Union[HeadersLike, Callable[[str, Headers], HeadersLike]] -HTTPResponse = Tuple[http.HTTPStatus, HeadersLike, bytes] +HTTPResponse = Tuple[StatusLike, HeadersLike, bytes] class WebSocketServerProtocol(WebSocketCommonProtocol): @@ -73,7 +73,7 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): await process(message) The iterator exits normally when the connection is closed with close code - 1000 (OK) or 1001 (going away). It raises + 1000 (OK) or 1001 (going away) or without a close code. It raises a :exc:`~websockets.exceptions.ConnectionClosedError` when the connection is closed with any other code. @@ -84,7 +84,7 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): ws_server: WebSocket server that created this connection. See :func:`serve` for the documentation of ``ws_handler``, ``logger``, ``origins``, - ``extensions``, ``subprotocols``, and ``extra_headers``. + ``extensions``, ``subprotocols``, ``extra_headers``, and ``server_header``. See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``, @@ -108,12 +108,14 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): extensions: Optional[Sequence[ServerExtensionFactory]] = None, subprotocols: Optional[Sequence[Subprotocol]] = None, extra_headers: Optional[HeadersLikeOrCallable] = None, + server_header: Optional[str] = USER_AGENT, process_request: Optional[ Callable[[str, Headers], Awaitable[Optional[HTTPResponse]]] ] = None, select_subprotocol: Optional[ Callable[[Sequence[Subprotocol], Sequence[Subprotocol]], Subprotocol] ] = None, + open_timeout: Optional[float] = 10, **kwargs: Any, ) -> None: if logger is None: @@ -132,8 +134,10 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): self.available_extensions = extensions self.available_subprotocols = subprotocols self.extra_headers = extra_headers + self.server_header = server_header self._process_request = process_request self._select_subprotocol = select_subprotocol + self.open_timeout = open_timeout def connection_made(self, transport: asyncio.BaseTransport) -> None: """ @@ -153,22 +157,20 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): Handle the lifecycle of a WebSocket connection. Since this method doesn't have a caller able to handle exceptions, it - attemps to log relevant ones and guarantees that the TCP connection is + attempts to log relevant ones and guarantees that the TCP connection is closed before exiting. """ try: - try: - await self.handshake( - origins=self.origins, - available_extensions=self.available_extensions, - available_subprotocols=self.available_subprotocols, - extra_headers=self.extra_headers, - ) - # Remove this branch when dropping support for Python < 3.8 - # because CancelledError no longer inherits Exception. - except asyncio.CancelledError: # pragma: no cover + async with asyncio_timeout(self.open_timeout): + await self.handshake( + origins=self.origins, + available_extensions=self.available_extensions, + available_subprotocols=self.available_subprotocols, + extra_headers=self.extra_headers, + ) + except asyncio.TimeoutError: # pragma: no cover raise except ConnectionError: raise @@ -216,14 +218,16 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): ) headers.setdefault("Date", email.utils.formatdate(usegmt=True)) - headers.setdefault("Server", USER_AGENT) + if self.server_header is not None: + headers.setdefault("Server", self.server_header) + headers.setdefault("Content-Length", str(len(body))) headers.setdefault("Content-Type", "text/plain") headers.setdefault("Connection", "close") self.write_http_response(status, headers, body) self.logger.info( - "connection failed (%d %s)", status.value, status.phrase + "connection rejected (%d %s)", status.value, status.phrase ) await self.close_transport() return @@ -325,9 +329,9 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): You may override this method in a :class:`WebSocketServerProtocol` subclass, for example: - * to return a HTTP 200 OK response on a given path; then a load + * to return an HTTP 200 OK response on a given path; then a load balancer can use this path for a health check; - * to authenticate the request and return a HTTP 401 Unauthorized or a + * to authenticate the request and return an HTTP 401 Unauthorized or an HTTP 403 Forbidden when authentication fails. You may also override this method with the ``process_request`` @@ -345,7 +349,7 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): request_headers: request headers. Returns: - Optional[Tuple[http.HTTPStatus, HeadersLike, bytes]]: :obj:`None` + Optional[Tuple[StatusLike, HeadersLike, bytes]]: :obj:`None` to continue the WebSocket handshake normally. An HTTP response, represented by a 3-uple of the response status, @@ -439,15 +443,12 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): header_values = headers.get_all("Sec-WebSocket-Extensions") if header_values and available_extensions: - parsed_header_values: List[ExtensionHeader] = sum( [parse_extension(header_value) for header_value in header_values], [] ) for name, request_params in parsed_header_values: - for ext_factory in available_extensions: - # Skip non-matching extensions based on their name. if ext_factory.name != name: continue @@ -499,7 +500,6 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): header_values = headers.get_all("Sec-WebSocket-Protocol") if header_values and available_subprotocols: - parsed_header_values: List[Subprotocol] = sum( [parse_subprotocol(header_value) for header_value in header_values], [] ) @@ -516,31 +516,29 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): server_subprotocols: Sequence[Subprotocol], ) -> Optional[Subprotocol]: """ - Pick a subprotocol among those offered by the client. + Pick a subprotocol among those supported by the client and the server. - If several subprotocols are supported by the client and the server, - the default implementation selects the preferred subprotocol by - giving equal value to the priorities of the client and the server. - If no subprotocol is supported by the client and the server, it - proceeds without a subprotocol. + If several subprotocols are available, select the preferred subprotocol + by giving equal weight to the preferences of the client and the server. - This is unlikely to be the most useful implementation in practice. - Many servers providing a subprotocol will require that the client - uses that subprotocol. Such rules can be implemented in a subclass. + If no subprotocol is available, proceed without a subprotocol. - You may also override this method with the ``select_subprotocol`` - argument of :func:`serve` and :class:`WebSocketServerProtocol`. + You may provide a ``select_subprotocol`` argument to :func:`serve` or + :class:`WebSocketServerProtocol` to override this logic. For example, + you could reject the handshake if the client doesn't support a + particular subprotocol, rather than accept the handshake without that + subprotocol. Args: client_subprotocols: list of subprotocols offered by the client. server_subprotocols: list of subprotocols available on the server. Returns: - Optional[Subprotocol]: Selected subprotocol. + Optional[Subprotocol]: Selected subprotocol, if a common subprotocol + was found. :obj:`None` to continue without a subprotocol. - """ if self._select_subprotocol is not None: return self._select_subprotocol(client_subprotocols, server_subprotocols) @@ -548,10 +546,10 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): subprotocols = set(client_subprotocols) & set(server_subprotocols) if not subprotocols: return None - priority = lambda p: ( - client_subprotocols.index(p) + server_subprotocols.index(p) - ) - return sorted(subprotocols, key=priority)[0] + return sorted( + subprotocols, + key=lambda p: client_subprotocols.index(p) + server_subprotocols.index(p), + )[0] async def handshake( self, @@ -594,7 +592,8 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): # The connection may drop while process_request is running. if self.state is State.CLOSED: - raise self.connection_closed_exc() # pragma: no cover + # This subclass of ConnectionError is silently ignored in handler(). + raise BrokenPipeError("connection closed during opening handshake") # Change the response to a 503 error if the server is shutting down. if not self.ws_server.is_serving(): @@ -635,7 +634,8 @@ class WebSocketServerProtocol(WebSocketCommonProtocol): response_headers.update(extra_headers) response_headers.setdefault("Date", email.utils.formatdate(usegmt=True)) - response_headers.setdefault("Server", USER_AGENT) + if self.server_header is not None: + response_headers.setdefault("Server", self.server_header) self.write_http_response(http.HTTPStatus.SWITCHING_PROTOCOLS, response_headers) @@ -658,9 +658,9 @@ class WebSocketServer: when shutting down. Args: - logger: logger for this server; - defaults to ``logging.getLogger("websockets.server")``; - see the :doc:`logging guide <../topics/logging>` for details. + logger: Logger for this server. + It defaults to ``logging.getLogger("websockets.server")``. + See the :doc:`logging guide <../../topics/logging>` for details. """ @@ -707,7 +707,7 @@ class WebSocketServer: self.logger.info("server listening on %s", name) # Initialized here because we need a reference to the event loop. - # This should be moved back to __init__ in Python 3.10. + # This should be moved back to __init__ when dropping Python < 3.10. self.closed_waiter = server.get_loop().create_future() def register(self, protocol: WebSocketServerProtocol) -> None: @@ -724,26 +724,30 @@ class WebSocketServer: """ self.websockets.remove(protocol) - def close(self) -> None: + def close(self, close_connections: bool = True) -> None: """ Close the server. - This method: + * Close the underlying :class:`~asyncio.Server`. + * When ``close_connections`` is :obj:`True`, which is the default, + close existing connections. Specifically: - * closes the underlying :class:`~asyncio.Server`; - * rejects new WebSocket connections with an HTTP 503 (service - unavailable) error; this happens when the server accepted the TCP - connection but didn't complete the WebSocket opening handshake prior - to closing; - * closes open WebSocket connections with close code 1001 (going away). + * Reject opening WebSocket connections with an HTTP 503 (service + unavailable) error. This happens when the server accepted the TCP + connection but didn't complete the opening handshake before closing. + * Close open WebSocket connections with close code 1001 (going away). + + * Wait until all connection handlers terminate. :meth:`close` is idempotent. """ if self.close_task is None: - self.close_task = self.get_loop().create_task(self._close()) + self.close_task = self.get_loop().create_task( + self._close(close_connections) + ) - async def _close(self) -> None: + async def _close(self, close_connections: bool) -> None: """ Implementation of :meth:`close`. @@ -757,36 +761,30 @@ class WebSocketServer: # Stop accepting new connections. self.server.close() - # Wait until self.server.close() completes. - await self.server.wait_closed() - # Wait until all accepted connections reach connection_made() and call # register(). See https://bugs.python.org/issue34852 for details. - await asyncio.sleep(0, **loop_if_py_lt_38(self.get_loop())) - - # Close OPEN connections with status code 1001. Since the server was - # closed, handshake() closes OPENING connections with a HTTP 503 - # error. Wait until all connections are closed. - - close_tasks = [ - asyncio.create_task(websocket.close(1001)) - for websocket in self.websockets - if websocket.state is not State.CONNECTING - ] - # asyncio.wait doesn't accept an empty first argument. - if close_tasks: - await asyncio.wait( - close_tasks, - **loop_if_py_lt_38(self.get_loop()), - ) - - # Wait until all connection handlers are complete. + await asyncio.sleep(0) + + if close_connections: + # Close OPEN connections with close code 1001. After server.close(), + # handshake() closes OPENING connections with an HTTP 503 error. + close_tasks = [ + asyncio.create_task(websocket.close(1001)) + for websocket in self.websockets + if websocket.state is not State.CONNECTING + ] + # asyncio.wait doesn't accept an empty first argument. + if close_tasks: + await asyncio.wait(close_tasks) + + # Wait until all TCP connections are closed. + await self.server.wait_closed() + # Wait until all connection handlers terminate. # asyncio.wait doesn't accept an empty first argument. if self.websockets: await asyncio.wait( - [websocket.handler_task for websocket in self.websockets], - **loop_if_py_lt_38(self.get_loop()), + [websocket.handler_task for websocket in self.websockets] ) # Tell wait_closed() to return. @@ -829,19 +827,37 @@ class WebSocketServer: """ return self.server.is_serving() - async def start_serving(self) -> None: + async def start_serving(self) -> None: # pragma: no cover """ See :meth:`asyncio.Server.start_serving`. + Typical use:: + + server = await serve(..., start_serving=False) + # perform additional setup here... + # ... then start the server + await server.start_serving() + """ - await self.server.start_serving() # pragma: no cover + await self.server.start_serving() - async def serve_forever(self) -> None: + async def serve_forever(self) -> None: # pragma: no cover """ See :meth:`asyncio.Server.serve_forever`. + Typical use:: + + server = await serve(...) + # this coroutine doesn't return + # canceling it stops the server + await server.serve_forever() + + This is an alternative to using :func:`serve` as an asynchronous context + manager. Shutdown is triggered by canceling :meth:`serve_forever` + instead of exiting a :func:`serve` context. + """ - await self.server.serve_forever() # pragma: no cover + await self.server.serve_forever() @property def sockets(self) -> Iterable[socket.socket]: @@ -851,17 +867,17 @@ class WebSocketServer: """ return self.server.sockets - async def __aenter__(self) -> WebSocketServer: - return self # pragma: no cover + async def __aenter__(self) -> WebSocketServer: # pragma: no cover + return self async def __aexit__( self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType], - ) -> None: - self.close() # pragma: no cover - await self.wait_closed() # pragma: no cover + ) -> None: # pragma: no cover + self.close() + await self.wait_closed() class Serve: @@ -879,53 +895,61 @@ class Serve: server performs the closing handshake and closes the connection. Awaiting :func:`serve` yields a :class:`WebSocketServer`. This object - provides :meth:`~WebSocketServer.close` and - :meth:`~WebSocketServer.wait_closed` methods for shutting down the server. + provides a :meth:`~WebSocketServer.close` method to shut down the server:: - :func:`serve` can be used as an asynchronous context manager:: + stop = asyncio.Future() # set this future to exit the server + + server = await serve(...) + await stop + await server.close() + + :func:`serve` can be used as an asynchronous context manager. Then, the + server is shut down automatically when exiting the context:: stop = asyncio.Future() # set this future to exit the server async with serve(...): await stop - The server is shut down automatically when exiting the context. - Args: - ws_handler: connection handler. It receives the WebSocket connection, + ws_handler: Connection handler. It receives the WebSocket connection, which is a :class:`WebSocketServerProtocol`, in argument. - host: network interfaces the server is bound to; - see :meth:`~asyncio.loop.create_server` for details. - port: TCP port the server listens on; - see :meth:`~asyncio.loop.create_server` for details. - create_protocol: factory for the :class:`asyncio.Protocol` managing - the connection; defaults to :class:`WebSocketServerProtocol`; may - be set to a wrapper or a subclass to customize connection handling. - logger: logger for this server; - defaults to ``logging.getLogger("websockets.server")``; - see the :doc:`logging guide <../topics/logging>` for details. - compression: shortcut that enables the "permessage-deflate" extension - by default; may be set to :obj:`None` to disable compression; - see the :doc:`compression guide <../topics/compression>` for details. - origins: acceptable values of the ``Origin`` header; include - :obj:`None` in the list if the lack of an origin is acceptable. - This is useful for defending against Cross-Site WebSocket - Hijacking attacks. - extensions: list of supported extensions, in order in which they - should be tried. - subprotocols: list of supported subprotocols, in order of decreasing + host: Network interfaces the server binds to. + See :meth:`~asyncio.loop.create_server` for details. + port: TCP port the server listens on. + See :meth:`~asyncio.loop.create_server` for details. + create_protocol: Factory for the :class:`asyncio.Protocol` managing + the connection. It defaults to :class:`WebSocketServerProtocol`. + Set it to a wrapper or a subclass to customize connection handling. + logger: Logger for this server. + It defaults to ``logging.getLogger("websockets.server")``. + See the :doc:`logging guide <../../topics/logging>` for details. + compression: The "permessage-deflate" extension is enabled by default. + Set ``compression`` to :obj:`None` to disable it. See the + :doc:`compression guide <../../topics/compression>` for details. + origins: Acceptable values of the ``Origin`` header, for defending + against Cross-Site WebSocket Hijacking attacks. Include :obj:`None` + in the list if the lack of an origin is acceptable. + extensions: List of supported extensions, in order in which they + should be negotiated and run. + subprotocols: List of supported subprotocols, in order of decreasing preference. extra_headers (Union[HeadersLike, Callable[[str, Headers], HeadersLike]]): - arbitrary HTTP headers to add to the request; this can be + Arbitrary HTTP headers to add to the response. This can be a :data:`~websockets.datastructures.HeadersLike` or a callable taking the request path and headers in arguments and returning a :data:`~websockets.datastructures.HeadersLike`. + server_header: Value of the ``Server`` response header. + It defaults to ``"Python/x.y.z websockets/X.Y"``. + Setting it to :obj:`None` removes the header. process_request (Optional[Callable[[str, Headers], \ - Awaitable[Optional[Tuple[http.HTTPStatus, HeadersLike, bytes]]]]]): - intercept HTTP request before the opening handshake; - see :meth:`~WebSocketServerProtocol.process_request` for details. - select_subprotocol: select a subprotocol supported by the client; - see :meth:`~WebSocketServerProtocol.select_subprotocol` for details. + Awaitable[Optional[Tuple[StatusLike, HeadersLike, bytes]]]]]): + Intercept HTTP request before the opening handshake. + See :meth:`~WebSocketServerProtocol.process_request` for details. + select_subprotocol: Select a subprotocol supported by the client. + See :meth:`~WebSocketServerProtocol.select_subprotocol` for details. + open_timeout: Timeout for opening connections in seconds. + :obj:`None` disables the timeout. See :class:`~websockets.legacy.protocol.WebSocketCommonProtocol` for the documentation of ``ping_interval``, ``ping_timeout``, ``close_timeout``, @@ -955,19 +979,21 @@ class Serve: host: Optional[Union[str, Sequence[str]]] = None, port: Optional[int] = None, *, - create_protocol: Optional[Callable[[Any], WebSocketServerProtocol]] = None, + create_protocol: Optional[Callable[..., WebSocketServerProtocol]] = None, logger: Optional[LoggerLike] = None, compression: Optional[str] = "deflate", origins: Optional[Sequence[Optional[Origin]]] = None, extensions: Optional[Sequence[ServerExtensionFactory]] = None, subprotocols: Optional[Sequence[Subprotocol]] = None, extra_headers: Optional[HeadersLikeOrCallable] = None, + server_header: Optional[str] = USER_AGENT, process_request: Optional[ Callable[[str, Headers], Awaitable[Optional[HTTPResponse]]] ] = None, select_subprotocol: Optional[ Callable[[Sequence[Subprotocol], Sequence[Subprotocol]], Subprotocol] ] = None, + open_timeout: Optional[float] = 10, ping_interval: Optional[float] = 20, ping_timeout: Optional[float] = 20, close_timeout: Optional[float] = None, @@ -1030,6 +1056,7 @@ class Serve: host=host, port=port, secure=secure, + open_timeout=open_timeout, ping_interval=ping_interval, ping_timeout=ping_timeout, close_timeout=close_timeout, @@ -1043,6 +1070,7 @@ class Serve: extensions=extensions, subprotocols=subprotocols, extra_headers=extra_headers, + server_header=server_header, process_request=process_request, select_subprotocol=select_subprotocol, logger=logger, @@ -1106,17 +1134,18 @@ def unix_serve( **kwargs: Any, ) -> Serve: """ - Similar to :func:`serve`, but for listening on Unix sockets. + Start a WebSocket server listening on a Unix socket. - This function builds upon the event - loop's :meth:`~asyncio.loop.create_unix_server` method. + This function is identical to :func:`serve`, except the ``host`` and + ``port`` arguments are replaced by ``path``. It is only available on Unix. - It is only available on Unix. + Unrecognized keyword arguments are passed the event loop's + :meth:`~asyncio.loop.create_unix_server` method. It's useful for deploying a server behind a reverse proxy such as nginx. Args: - path: file system path to the Unix socket. + path: File system path to the Unix socket. """ return serve(ws_handler, path=path, unix=True, **kwargs) diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/protocol.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/protocol.py new file mode 100644 index 00000000000..765e6b9bb4b --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/protocol.py @@ -0,0 +1,708 @@ +from __future__ import annotations + +import enum +import logging +import uuid +from typing import Generator, List, Optional, Type, Union + +from .exceptions import ( + ConnectionClosed, + ConnectionClosedError, + ConnectionClosedOK, + InvalidState, + PayloadTooBig, + ProtocolError, +) +from .extensions import Extension +from .frames import ( + OK_CLOSE_CODES, + OP_BINARY, + OP_CLOSE, + OP_CONT, + OP_PING, + OP_PONG, + OP_TEXT, + Close, + CloseCode, + Frame, +) +from .http11 import Request, Response +from .streams import StreamReader +from .typing import LoggerLike, Origin, Subprotocol + + +__all__ = [ + "Protocol", + "Side", + "State", + "SEND_EOF", +] + +Event = Union[Request, Response, Frame] +"""Events that :meth:`~Protocol.events_received` may return.""" + + +class Side(enum.IntEnum): + """A WebSocket connection is either a server or a client.""" + + SERVER, CLIENT = range(2) + + +SERVER = Side.SERVER +CLIENT = Side.CLIENT + + +class State(enum.IntEnum): + """A WebSocket connection is in one of these four states.""" + + CONNECTING, OPEN, CLOSING, CLOSED = range(4) + + +CONNECTING = State.CONNECTING +OPEN = State.OPEN +CLOSING = State.CLOSING +CLOSED = State.CLOSED + + +SEND_EOF = b"" +"""Sentinel signaling that the TCP connection must be half-closed.""" + + +class Protocol: + """ + Sans-I/O implementation of a WebSocket connection. + + Args: + side: :attr:`~Side.CLIENT` or :attr:`~Side.SERVER`. + state: initial state of the WebSocket connection. + max_size: maximum size of incoming messages in bytes; + :obj:`None` disables the limit. + logger: logger for this connection; depending on ``side``, + defaults to ``logging.getLogger("websockets.client")`` + or ``logging.getLogger("websockets.server")``; + see the :doc:`logging guide <../../topics/logging>` for details. + + """ + + def __init__( + self, + side: Side, + *, + state: State = OPEN, + max_size: Optional[int] = 2**20, + logger: Optional[LoggerLike] = None, + ) -> None: + # Unique identifier. For logs. + self.id: uuid.UUID = uuid.uuid4() + """Unique identifier of the connection. Useful in logs.""" + + # Logger or LoggerAdapter for this connection. + if logger is None: + logger = logging.getLogger(f"websockets.{side.name.lower()}") + self.logger: LoggerLike = logger + """Logger for this connection.""" + + # Track if DEBUG is enabled. Shortcut logging calls if it isn't. + self.debug = logger.isEnabledFor(logging.DEBUG) + + # Connection side. CLIENT or SERVER. + self.side = side + + # Connection state. Initially OPEN because subclasses handle CONNECTING. + self.state = state + + # Maximum size of incoming messages in bytes. + self.max_size = max_size + + # Current size of incoming message in bytes. Only set while reading a + # fragmented message i.e. a data frames with the FIN bit not set. + self.cur_size: Optional[int] = None + + # True while sending a fragmented message i.e. a data frames with the + # FIN bit not set. + self.expect_continuation_frame = False + + # WebSocket protocol parameters. + self.origin: Optional[Origin] = None + self.extensions: List[Extension] = [] + self.subprotocol: Optional[Subprotocol] = None + + # Close code and reason, set when a close frame is sent or received. + self.close_rcvd: Optional[Close] = None + self.close_sent: Optional[Close] = None + self.close_rcvd_then_sent: Optional[bool] = None + + # Track if an exception happened during the handshake. + self.handshake_exc: Optional[Exception] = None + """ + Exception to raise if the opening handshake failed. + + :obj:`None` if the opening handshake succeeded. + + """ + + # Track if send_eof() was called. + self.eof_sent = False + + # Parser state. + self.reader = StreamReader() + self.events: List[Event] = [] + self.writes: List[bytes] = [] + self.parser = self.parse() + next(self.parser) # start coroutine + self.parser_exc: Optional[Exception] = None + + @property + def state(self) -> State: + """ + WebSocket connection state. + + Defined in 4.1, 4.2, 7.1.3, and 7.1.4 of :rfc:`6455`. + + """ + return self._state + + @state.setter + def state(self, state: State) -> None: + if self.debug: + self.logger.debug("= connection is %s", state.name) + self._state = state + + @property + def close_code(self) -> Optional[int]: + """ + `WebSocket close code`_. + + .. _WebSocket close code: + https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.5 + + :obj:`None` if the connection isn't closed yet. + + """ + if self.state is not CLOSED: + return None + elif self.close_rcvd is None: + return CloseCode.ABNORMAL_CLOSURE + else: + return self.close_rcvd.code + + @property + def close_reason(self) -> Optional[str]: + """ + `WebSocket close reason`_. + + .. _WebSocket close reason: + https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.6 + + :obj:`None` if the connection isn't closed yet. + + """ + if self.state is not CLOSED: + return None + elif self.close_rcvd is None: + return "" + else: + return self.close_rcvd.reason + + @property + def close_exc(self) -> ConnectionClosed: + """ + Exception to raise when trying to interact with a closed connection. + + Don't raise this exception while the connection :attr:`state` + is :attr:`~websockets.protocol.State.CLOSING`; wait until + it's :attr:`~websockets.protocol.State.CLOSED`. + + Indeed, the exception includes the close code and reason, which are + known only once the connection is closed. + + Raises: + AssertionError: if the connection isn't closed yet. + + """ + assert self.state is CLOSED, "connection isn't closed yet" + exc_type: Type[ConnectionClosed] + if ( + self.close_rcvd is not None + and self.close_sent is not None + and self.close_rcvd.code in OK_CLOSE_CODES + and self.close_sent.code in OK_CLOSE_CODES + ): + exc_type = ConnectionClosedOK + else: + exc_type = ConnectionClosedError + exc: ConnectionClosed = exc_type( + self.close_rcvd, + self.close_sent, + self.close_rcvd_then_sent, + ) + # Chain to the exception raised in the parser, if any. + exc.__cause__ = self.parser_exc + return exc + + # Public methods for receiving data. + + def receive_data(self, data: bytes) -> None: + """ + Receive data from the network. + + After calling this method: + + - You must call :meth:`data_to_send` and send this data to the network. + - You should call :meth:`events_received` and process resulting events. + + Raises: + EOFError: if :meth:`receive_eof` was called earlier. + + """ + self.reader.feed_data(data) + next(self.parser) + + def receive_eof(self) -> None: + """ + Receive the end of the data stream from the network. + + After calling this method: + + - You must call :meth:`data_to_send` and send this data to the network; + it will return ``[b""]``, signaling the end of the stream, or ``[]``. + - You aren't expected to call :meth:`events_received`; it won't return + any new events. + + Raises: + EOFError: if :meth:`receive_eof` was called earlier. + + """ + self.reader.feed_eof() + next(self.parser) + + # Public methods for sending events. + + def send_continuation(self, data: bytes, fin: bool) -> None: + """ + Send a `Continuation frame`_. + + .. _Continuation frame: + https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 + + Parameters: + data: payload containing the same kind of data + as the initial frame. + fin: FIN bit; set it to :obj:`True` if this is the last frame + of a fragmented message and to :obj:`False` otherwise. + + Raises: + ProtocolError: if a fragmented message isn't in progress. + + """ + if not self.expect_continuation_frame: + raise ProtocolError("unexpected continuation frame") + self.expect_continuation_frame = not fin + self.send_frame(Frame(OP_CONT, data, fin)) + + def send_text(self, data: bytes, fin: bool = True) -> None: + """ + Send a `Text frame`_. + + .. _Text frame: + https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 + + Parameters: + data: payload containing text encoded with UTF-8. + fin: FIN bit; set it to :obj:`False` if this is the first frame of + a fragmented message. + + Raises: + ProtocolError: if a fragmented message is in progress. + + """ + if self.expect_continuation_frame: + raise ProtocolError("expected a continuation frame") + self.expect_continuation_frame = not fin + self.send_frame(Frame(OP_TEXT, data, fin)) + + def send_binary(self, data: bytes, fin: bool = True) -> None: + """ + Send a `Binary frame`_. + + .. _Binary frame: + https://datatracker.ietf.org/doc/html/rfc6455#section-5.6 + + Parameters: + data: payload containing arbitrary binary data. + fin: FIN bit; set it to :obj:`False` if this is the first frame of + a fragmented message. + + Raises: + ProtocolError: if a fragmented message is in progress. + + """ + if self.expect_continuation_frame: + raise ProtocolError("expected a continuation frame") + self.expect_continuation_frame = not fin + self.send_frame(Frame(OP_BINARY, data, fin)) + + def send_close(self, code: Optional[int] = None, reason: str = "") -> None: + """ + Send a `Close frame`_. + + .. _Close frame: + https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.1 + + Parameters: + code: close code. + reason: close reason. + + Raises: + ProtocolError: if a fragmented message is being sent, if the code + isn't valid, or if a reason is provided without a code + + """ + if self.expect_continuation_frame: + raise ProtocolError("expected a continuation frame") + if code is None: + if reason != "": + raise ProtocolError("cannot send a reason without a code") + close = Close(CloseCode.NO_STATUS_RCVD, "") + data = b"" + else: + close = Close(code, reason) + data = close.serialize() + # send_frame() guarantees that self.state is OPEN at this point. + # 7.1.3. The WebSocket Closing Handshake is Started + self.send_frame(Frame(OP_CLOSE, data)) + self.close_sent = close + self.state = CLOSING + + def send_ping(self, data: bytes) -> None: + """ + Send a `Ping frame`_. + + .. _Ping frame: + https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.2 + + Parameters: + data: payload containing arbitrary binary data. + + """ + self.send_frame(Frame(OP_PING, data)) + + def send_pong(self, data: bytes) -> None: + """ + Send a `Pong frame`_. + + .. _Pong frame: + https://datatracker.ietf.org/doc/html/rfc6455#section-5.5.3 + + Parameters: + data: payload containing arbitrary binary data. + + """ + self.send_frame(Frame(OP_PONG, data)) + + def fail(self, code: int, reason: str = "") -> None: + """ + `Fail the WebSocket connection`_. + + .. _Fail the WebSocket connection: + https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.7 + + Parameters: + code: close code + reason: close reason + + Raises: + ProtocolError: if the code isn't valid. + """ + # 7.1.7. Fail the WebSocket Connection + + # Send a close frame when the state is OPEN (a close frame was already + # sent if it's CLOSING), except when failing the connection because + # of an error reading from or writing to the network. + if self.state is OPEN: + if code != CloseCode.ABNORMAL_CLOSURE: + close = Close(code, reason) + data = close.serialize() + self.send_frame(Frame(OP_CLOSE, data)) + self.close_sent = close + self.state = CLOSING + + # When failing the connection, a server closes the TCP connection + # without waiting for the client to complete the handshake, while a + # client waits for the server to close the TCP connection, possibly + # after sending a close frame that the client will ignore. + if self.side is SERVER and not self.eof_sent: + self.send_eof() + + # 7.1.7. Fail the WebSocket Connection "An endpoint MUST NOT continue + # to attempt to process data(including a responding Close frame) from + # the remote endpoint after being instructed to _Fail the WebSocket + # Connection_." + self.parser = self.discard() + next(self.parser) # start coroutine + + # Public method for getting incoming events after receiving data. + + def events_received(self) -> List[Event]: + """ + Fetch events generated from data received from the network. + + Call this method immediately after any of the ``receive_*()`` methods. + + Process resulting events, likely by passing them to the application. + + Returns: + List[Event]: Events read from the connection. + """ + events, self.events = self.events, [] + return events + + # Public method for getting outgoing data after receiving data or sending events. + + def data_to_send(self) -> List[bytes]: + """ + Obtain data to send to the network. + + Call this method immediately after any of the ``receive_*()``, + ``send_*()``, or :meth:`fail` methods. + + Write resulting data to the connection. + + The empty bytestring :data:`~websockets.protocol.SEND_EOF` signals + the end of the data stream. When you receive it, half-close the TCP + connection. + + Returns: + List[bytes]: Data to write to the connection. + + """ + writes, self.writes = self.writes, [] + return writes + + def close_expected(self) -> bool: + """ + Tell if the TCP connection is expected to close soon. + + Call this method immediately after any of the ``receive_*()``, + ``send_close()``, or :meth:`fail` methods. + + If it returns :obj:`True`, schedule closing the TCP connection after a + short timeout if the other side hasn't already closed it. + + Returns: + bool: Whether the TCP connection is expected to close soon. + + """ + # We expect a TCP close if and only if we sent a close frame: + # * Normal closure: once we send a close frame, we expect a TCP close: + # server waits for client to complete the TCP closing handshake; + # client waits for server to initiate the TCP closing handshake. + # * Abnormal closure: we always send a close frame and the same logic + # applies, except on EOFError where we don't send a close frame + # because we already received the TCP close, so we don't expect it. + # We already got a TCP Close if and only if the state is CLOSED. + return self.state is CLOSING or self.handshake_exc is not None + + # Private methods for receiving data. + + def parse(self) -> Generator[None, None, None]: + """ + Parse incoming data into frames. + + :meth:`receive_data` and :meth:`receive_eof` run this generator + coroutine until it needs more data or reaches EOF. + + :meth:`parse` never raises an exception. Instead, it sets the + :attr:`parser_exc` and yields control. + + """ + try: + while True: + if (yield from self.reader.at_eof()): + if self.debug: + self.logger.debug("< EOF") + # If the WebSocket connection is closed cleanly, with a + # closing handhshake, recv_frame() substitutes parse() + # with discard(). This branch is reached only when the + # connection isn't closed cleanly. + raise EOFError("unexpected end of stream") + + if self.max_size is None: + max_size = None + elif self.cur_size is None: + max_size = self.max_size + else: + max_size = self.max_size - self.cur_size + + # During a normal closure, execution ends here on the next + # iteration of the loop after receiving a close frame. At + # this point, recv_frame() replaced parse() by discard(). + frame = yield from Frame.parse( + self.reader.read_exact, + mask=self.side is SERVER, + max_size=max_size, + extensions=self.extensions, + ) + + if self.debug: + self.logger.debug("< %s", frame) + + self.recv_frame(frame) + + except ProtocolError as exc: + self.fail(CloseCode.PROTOCOL_ERROR, str(exc)) + self.parser_exc = exc + + except EOFError as exc: + self.fail(CloseCode.ABNORMAL_CLOSURE, str(exc)) + self.parser_exc = exc + + except UnicodeDecodeError as exc: + self.fail(CloseCode.INVALID_DATA, f"{exc.reason} at position {exc.start}") + self.parser_exc = exc + + except PayloadTooBig as exc: + self.fail(CloseCode.MESSAGE_TOO_BIG, str(exc)) + self.parser_exc = exc + + except Exception as exc: + self.logger.error("parser failed", exc_info=True) + # Don't include exception details, which may be security-sensitive. + self.fail(CloseCode.INTERNAL_ERROR) + self.parser_exc = exc + + # During an abnormal closure, execution ends here after catching an + # exception. At this point, fail() replaced parse() by discard(). + yield + raise AssertionError("parse() shouldn't step after error") + + def discard(self) -> Generator[None, None, None]: + """ + Discard incoming data. + + This coroutine replaces :meth:`parse`: + + - after receiving a close frame, during a normal closure (1.4); + - after sending a close frame, during an abnormal closure (7.1.7). + + """ + # The server close the TCP connection in the same circumstances where + # discard() replaces parse(). The client closes the connection later, + # after the server closes the connection or a timeout elapses. + # (The latter case cannot be handled in this Sans-I/O layer.) + assert (self.side is SERVER) == (self.eof_sent) + while not (yield from self.reader.at_eof()): + self.reader.discard() + if self.debug: + self.logger.debug("< EOF") + # A server closes the TCP connection immediately, while a client + # waits for the server to close the TCP connection. + if self.side is CLIENT: + self.send_eof() + self.state = CLOSED + # If discard() completes normally, execution ends here. + yield + # Once the reader reaches EOF, its feed_data/eof() methods raise an + # error, so our receive_data/eof() methods don't step the generator. + raise AssertionError("discard() shouldn't step after EOF") + + def recv_frame(self, frame: Frame) -> None: + """ + Process an incoming frame. + + """ + if frame.opcode is OP_TEXT or frame.opcode is OP_BINARY: + if self.cur_size is not None: + raise ProtocolError("expected a continuation frame") + if frame.fin: + self.cur_size = None + else: + self.cur_size = len(frame.data) + + elif frame.opcode is OP_CONT: + if self.cur_size is None: + raise ProtocolError("unexpected continuation frame") + if frame.fin: + self.cur_size = None + else: + self.cur_size += len(frame.data) + + elif frame.opcode is OP_PING: + # 5.5.2. Ping: "Upon receipt of a Ping frame, an endpoint MUST + # send a Pong frame in response" + pong_frame = Frame(OP_PONG, frame.data) + self.send_frame(pong_frame) + + elif frame.opcode is OP_PONG: + # 5.5.3 Pong: "A response to an unsolicited Pong frame is not + # expected." + pass + + elif frame.opcode is OP_CLOSE: + # 7.1.5. The WebSocket Connection Close Code + # 7.1.6. The WebSocket Connection Close Reason + self.close_rcvd = Close.parse(frame.data) + if self.state is CLOSING: + assert self.close_sent is not None + self.close_rcvd_then_sent = False + + if self.cur_size is not None: + raise ProtocolError("incomplete fragmented message") + + # 5.5.1 Close: "If an endpoint receives a Close frame and did + # not previously send a Close frame, the endpoint MUST send a + # Close frame in response. (When sending a Close frame in + # response, the endpoint typically echos the status code it + # received.)" + + if self.state is OPEN: + # Echo the original data instead of re-serializing it with + # Close.serialize() because that fails when the close frame + # is empty and Close.parse() synthesizes a 1005 close code. + # The rest is identical to send_close(). + self.send_frame(Frame(OP_CLOSE, frame.data)) + self.close_sent = self.close_rcvd + self.close_rcvd_then_sent = True + self.state = CLOSING + + # 7.1.2. Start the WebSocket Closing Handshake: "Once an + # endpoint has both sent and received a Close control frame, + # that endpoint SHOULD _Close the WebSocket Connection_" + + # A server closes the TCP connection immediately, while a client + # waits for the server to close the TCP connection. + if self.side is SERVER: + self.send_eof() + + # 1.4. Closing Handshake: "after receiving a control frame + # indicating the connection should be closed, a peer discards + # any further data received." + self.parser = self.discard() + next(self.parser) # start coroutine + + else: + # This can't happen because Frame.parse() validates opcodes. + raise AssertionError(f"unexpected opcode: {frame.opcode:02x}") + + self.events.append(frame) + + # Private methods for sending events. + + def send_frame(self, frame: Frame) -> None: + if self.state is not OPEN: + raise InvalidState( + f"cannot write to a WebSocket in the {self.state.name} state" + ) + + if self.debug: + self.logger.debug("> %s", frame) + self.writes.append( + frame.serialize(mask=self.side is CLIENT, extensions=self.extensions) + ) + + def send_eof(self) -> None: + assert not self.eof_sent + self.eof_sent = True + if self.debug: + self.logger.debug("> EOF") + self.writes.append(SEND_EOF) diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/server.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/server.py index 5dad50b6a12..191660553ff 100644 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets/server.py +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/server.py @@ -4,9 +4,9 @@ import base64 import binascii import email.utils import http -from typing import Generator, List, Optional, Sequence, Tuple, cast +import warnings +from typing import Any, Callable, Generator, List, Optional, Sequence, Tuple, cast -from .connection import CONNECTING, OPEN, SERVER, Connection, State from .datastructures import Headers, MultipleValuesError from .exceptions import ( InvalidHandshake, @@ -25,13 +25,14 @@ from .headers import ( parse_subprotocol, parse_upgrade, ) -from .http import USER_AGENT from .http11 import Request, Response +from .protocol import CONNECTING, OPEN, SERVER, Protocol, State from .typing import ( ConnectionOption, ExtensionHeader, LoggerLike, Origin, + StatusLike, Subprotocol, UpgradeProtocol, ) @@ -39,13 +40,15 @@ from .utils import accept_key # See #940 for why lazy_import isn't used here for backwards compatibility. -from .legacy.server import * # isort:skip # noqa +# See #1400 for why listing compatibility imports in __all__ helps PyCharm. +from .legacy.server import * # isort:skip # noqa: I001 +from .legacy.server import __all__ as legacy__all__ -__all__ = ["ServerConnection"] +__all__ = ["ServerProtocol"] + legacy__all__ -class ServerConnection(Connection): +class ServerProtocol(Protocol): """ Sans-I/O implementation of a WebSocket server connection. @@ -58,20 +61,31 @@ class ServerConnection(Connection): should be tried. subprotocols: list of supported subprotocols, in order of decreasing preference. + select_subprotocol: Callback for selecting a subprotocol among + those supported by the client and the server. It has the same + signature as the :meth:`select_subprotocol` method, including a + :class:`ServerProtocol` instance as first argument. state: initial state of the WebSocket connection. max_size: maximum size of incoming messages in bytes; - :obj:`None` to disable the limit. + :obj:`None` disables the limit. logger: logger for this connection; defaults to ``logging.getLogger("websockets.client")``; - see the :doc:`logging guide <../topics/logging>` for details. + see the :doc:`logging guide <../../topics/logging>` for details. """ def __init__( self, + *, origins: Optional[Sequence[Optional[Origin]]] = None, extensions: Optional[Sequence[ServerExtensionFactory]] = None, subprotocols: Optional[Sequence[Subprotocol]] = None, + select_subprotocol: Optional[ + Callable[ + [ServerProtocol, Sequence[Subprotocol]], + Optional[Subprotocol], + ] + ] = None, state: State = CONNECTING, max_size: Optional[int] = 2**20, logger: Optional[LoggerLike] = None, @@ -85,6 +99,14 @@ class ServerConnection(Connection): self.origins = origins self.available_extensions = extensions self.available_subprotocols = subprotocols + if select_subprotocol is not None: + # Bind select_subprotocol then shadow self.select_subprotocol. + # Use setattr to work around https://github.com/python/mypy/issues/2427. + setattr( + self, + "select_subprotocol", + select_subprotocol.__get__(self, self.__class__), + ) def accept(self, request: Request) -> Response: """ @@ -95,13 +117,13 @@ class ServerConnection(Connection): You must send the handshake response with :meth:`send_response`. - You can modify it before sending it, for example to add HTTP headers. + You may modify it before sending it, for example to add HTTP headers. Args: request: WebSocket handshake request event received from the client. Returns: - Response: WebSocket handshake response event to send to the client. + WebSocket handshake response event to send to the client. """ try: @@ -145,6 +167,8 @@ class ServerConnection(Connection): f"Failed to open a WebSocket connection: {exc}.\n", ) except Exception as exc: + # Handle exceptions raised by user-provided select_subprotocol and + # unexpected errors. request._exception = exc self.handshake_exc = exc self.logger.error("opening handshake failed", exc_info=True) @@ -170,13 +194,12 @@ class ServerConnection(Connection): if protocol_header is not None: headers["Sec-WebSocket-Protocol"] = protocol_header - headers["Server"] = USER_AGENT - self.logger.info("connection open") return Response(101, "Switching Protocols", headers) def process_request( - self, request: Request + self, + request: Request, ) -> Tuple[str, Optional[str], Optional[str]]: """ Check a handshake request and negotiate extensions and subprotocol. @@ -274,6 +297,7 @@ class ServerConnection(Connection): Optional[Origin]: origin, if it is acceptable. Raises: + InvalidHandshake: if the Origin header is invalid. InvalidOrigin: if the origin isn't acceptable. """ @@ -298,8 +322,8 @@ class ServerConnection(Connection): Accept or reject each extension proposed in the client request. Negotiate parameters for accepted extensions. - :rfc:`6455` leaves the rules up to the specification of each - :extension. + Per :rfc:`6455`, negotiation rules are defined by the specification of + each extension. To provide this level of flexibility, for each extension proposed by the client, we check for a match with each extension available in the @@ -324,7 +348,7 @@ class ServerConnection(Connection): HTTP response header and list of accepted extensions. Raises: - InvalidHandshake: to abort the handshake with an HTTP 400 error. + InvalidHandshake: if the Sec-WebSocket-Extensions header is invalid. """ response_header_value: Optional[str] = None @@ -335,15 +359,12 @@ class ServerConnection(Connection): header_values = headers.get_all("Sec-WebSocket-Extensions") if header_values and self.available_extensions: - parsed_header_values: List[ExtensionHeader] = sum( [parse_extension(header_value) for header_value in header_values], [] ) for name, request_params in parsed_header_values: - for ext_factory in self.available_extensions: - # Skip non-matching extensions based on their name. if ext_factory.name != name: continue @@ -384,64 +405,83 @@ class ServerConnection(Connection): also the value of the ``Sec-WebSocket-Protocol`` response header. Raises: - InvalidHandshake: to abort the handshake with an HTTP 400 error. + InvalidHandshake: if the Sec-WebSocket-Subprotocol header is invalid. """ - subprotocol: Optional[Subprotocol] = None - - header_values = headers.get_all("Sec-WebSocket-Protocol") - - if header_values and self.available_subprotocols: - - parsed_header_values: List[Subprotocol] = sum( - [parse_subprotocol(header_value) for header_value in header_values], [] - ) - - subprotocol = self.select_subprotocol( - parsed_header_values, self.available_subprotocols - ) + subprotocols: Sequence[Subprotocol] = sum( + [ + parse_subprotocol(header_value) + for header_value in headers.get_all("Sec-WebSocket-Protocol") + ], + [], + ) - return subprotocol + return self.select_subprotocol(subprotocols) def select_subprotocol( self, - client_subprotocols: Sequence[Subprotocol], - server_subprotocols: Sequence[Subprotocol], + subprotocols: Sequence[Subprotocol], ) -> Optional[Subprotocol]: """ Pick a subprotocol among those offered by the client. - If several subprotocols are supported by the client and the server, - the default implementation selects the preferred subprotocols by - giving equal value to the priorities of the client and the server. + If several subprotocols are supported by both the client and the server, + pick the first one in the list declared the server. + + If the server doesn't support any subprotocols, continue without a + subprotocol, regardless of what the client offers. + + If the server supports at least one subprotocol and the client doesn't + offer any, abort the handshake with an HTTP 400 error. - If no common subprotocol is supported by the client and the server, it - proceeds without a subprotocol. + You provide a ``select_subprotocol`` argument to :class:`ServerProtocol` + to override this logic. For example, you could accept the connection + even if client doesn't offer a subprotocol, rather than reject it. - This is unlikely to be the most useful implementation in practice, as - many servers providing a subprotocol will require that the client uses - that subprotocol. + Here's how to negotiate the ``chat`` subprotocol if the client supports + it and continue without a subprotocol otherwise:: + + def select_subprotocol(protocol, subprotocols): + if "chat" in subprotocols: + return "chat" Args: - client_subprotocols: list of subprotocols offered by the client. - server_subprotocols: list of subprotocols available on the server. + subprotocols: list of subprotocols offered by the client. Returns: - Optional[Subprotocol]: Subprotocol, if a common subprotocol was - found. + Optional[Subprotocol]: Selected subprotocol, if a common subprotocol + was found. + + :obj:`None` to continue without a subprotocol. + + Raises: + NegotiationError: custom implementations may raise this exception + to abort the handshake with an HTTP 400 error. """ - subprotocols = set(client_subprotocols) & set(server_subprotocols) - if not subprotocols: + # Server doesn't offer any subprotocols. + if not self.available_subprotocols: # None or empty list return None - priority = lambda p: ( - client_subprotocols.index(p) + server_subprotocols.index(p) + + # Server offers at least one subprotocol but client doesn't offer any. + if not subprotocols: + raise NegotiationError("missing subprotocol") + + # Server and client both offer subprotocols. Look for a shared one. + proposed_subprotocols = set(subprotocols) + for subprotocol in self.available_subprotocols: + if subprotocol in proposed_subprotocols: + return subprotocol + + # No common subprotocol was found. + raise NegotiationError( + "invalid subprotocol; expected one of " + + ", ".join(self.available_subprotocols) ) - return sorted(subprotocols, key=priority)[0] def reject( self, - status: http.HTTPStatus, + status: StatusLike, text: str, ) -> Response: """ @@ -462,6 +502,8 @@ class ServerConnection(Connection): Response: WebSocket handshake response event to send to the client. """ + # If a user passes an int instead of a HTTPStatus, fix it automatically. + status = http.HTTPStatus(status) body = text.encode() headers = Headers( [ @@ -469,16 +511,15 @@ class ServerConnection(Connection): ("Connection", "close"), ("Content-Length", str(len(body))), ("Content-Type", "text/plain; charset=utf-8"), - ("Server", USER_AGENT), ] ) response = Response(status.value, status.phrase, headers, body) # When reject() is called from accept(), handshake_exc is already set. # If a user calls reject(), set handshake_exc to guarantee invariant: - # "handshake_exc is None if and only if opening handshake succeded." + # "handshake_exc is None if and only if opening handshake succeeded." if self.handshake_exc is None: self.handshake_exc = InvalidStatus(response) - self.logger.info("connection failed (%d %s)", status.value, status.phrase) + self.logger.info("connection rejected (%d %s)", status.value, status.phrase) return response def send_response(self, response: Response) -> None: @@ -509,7 +550,16 @@ class ServerConnection(Connection): def parse(self) -> Generator[None, None, None]: if self.state is CONNECTING: - request = yield from Request.parse(self.reader.read_line) + try: + request = yield from Request.parse( + self.reader.read_line, + ) + except Exception as exc: + self.handshake_exc = exc + self.send_eof() + self.parser = self.discard() + next(self.parser) # start coroutine + yield if self.debug: self.logger.debug("< GET %s HTTP/1.1", request.path) @@ -519,3 +569,12 @@ class ServerConnection(Connection): self.events.append(request) yield from super().parse() + + +class ServerConnection(ServerProtocol): + def __init__(self, *args: Any, **kwargs: Any) -> None: + warnings.warn( + "ServerConnection was renamed to ServerProtocol", + DeprecationWarning, + ) + super().__init__(*args, **kwargs) diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/speedups.pyi b/tests/wpt/tests/tools/third_party/websockets/src/websockets/speedups.pyi new file mode 100644 index 00000000000..821438a064e --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/speedups.pyi @@ -0,0 +1 @@ +def apply_mask(data: bytes, mask: bytes) -> bytes: ... diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/sync/__init__.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/sync/__init__.py new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/sync/__init__.py diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/sync/client.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/sync/client.py new file mode 100644 index 00000000000..087ff5f569a --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/sync/client.py @@ -0,0 +1,328 @@ +from __future__ import annotations + +import socket +import ssl +import threading +from typing import Any, Optional, Sequence, Type + +from ..client import ClientProtocol +from ..datastructures import HeadersLike +from ..extensions.base import ClientExtensionFactory +from ..extensions.permessage_deflate import enable_client_permessage_deflate +from ..headers import validate_subprotocols +from ..http import USER_AGENT +from ..http11 import Response +from ..protocol import CONNECTING, OPEN, Event +from ..typing import LoggerLike, Origin, Subprotocol +from ..uri import parse_uri +from .connection import Connection +from .utils import Deadline + + +__all__ = ["connect", "unix_connect", "ClientConnection"] + + +class ClientConnection(Connection): + """ + Threaded implementation of a WebSocket client connection. + + :class:`ClientConnection` provides :meth:`recv` and :meth:`send` methods for + receiving and sending messages. + + It supports iteration to receive messages:: + + for message in websocket: + process(message) + + The iterator exits normally when the connection is closed with close code + 1000 (OK) or 1001 (going away) or without a close code. It raises a + :exc:`~websockets.exceptions.ConnectionClosedError` when the connection is + closed with any other code. + + Args: + socket: Socket connected to a WebSocket server. + protocol: Sans-I/O connection. + close_timeout: Timeout for closing the connection in seconds. + + """ + + def __init__( + self, + socket: socket.socket, + protocol: ClientProtocol, + *, + close_timeout: Optional[float] = 10, + ) -> None: + self.protocol: ClientProtocol + self.response_rcvd = threading.Event() + super().__init__( + socket, + protocol, + close_timeout=close_timeout, + ) + + def handshake( + self, + additional_headers: Optional[HeadersLike] = None, + user_agent_header: Optional[str] = USER_AGENT, + timeout: Optional[float] = None, + ) -> None: + """ + Perform the opening handshake. + + """ + with self.send_context(expected_state=CONNECTING): + self.request = self.protocol.connect() + if additional_headers is not None: + self.request.headers.update(additional_headers) + if user_agent_header is not None: + self.request.headers["User-Agent"] = user_agent_header + self.protocol.send_request(self.request) + + if not self.response_rcvd.wait(timeout): + self.close_socket() + self.recv_events_thread.join() + raise TimeoutError("timed out during handshake") + + if self.response is None: + self.close_socket() + self.recv_events_thread.join() + raise ConnectionError("connection closed during handshake") + + if self.protocol.state is not OPEN: + self.recv_events_thread.join(self.close_timeout) + self.close_socket() + self.recv_events_thread.join() + + if self.protocol.handshake_exc is not None: + raise self.protocol.handshake_exc + + def process_event(self, event: Event) -> None: + """ + Process one incoming event. + + """ + # First event - handshake response. + if self.response is None: + assert isinstance(event, Response) + self.response = event + self.response_rcvd.set() + # Later events - frames. + else: + super().process_event(event) + + def recv_events(self) -> None: + """ + Read incoming data from the socket and process events. + + """ + try: + super().recv_events() + finally: + # If the connection is closed during the handshake, unblock it. + self.response_rcvd.set() + + +def connect( + uri: str, + *, + # TCP/TLS — unix and path are only for unix_connect() + sock: Optional[socket.socket] = None, + ssl_context: Optional[ssl.SSLContext] = None, + server_hostname: Optional[str] = None, + unix: bool = False, + path: Optional[str] = None, + # WebSocket + origin: Optional[Origin] = None, + extensions: Optional[Sequence[ClientExtensionFactory]] = None, + subprotocols: Optional[Sequence[Subprotocol]] = None, + additional_headers: Optional[HeadersLike] = None, + user_agent_header: Optional[str] = USER_AGENT, + compression: Optional[str] = "deflate", + # Timeouts + open_timeout: Optional[float] = 10, + close_timeout: Optional[float] = 10, + # Limits + max_size: Optional[int] = 2**20, + # Logging + logger: Optional[LoggerLike] = None, + # Escape hatch for advanced customization + create_connection: Optional[Type[ClientConnection]] = None, +) -> ClientConnection: + """ + Connect to the WebSocket server at ``uri``. + + This function returns a :class:`ClientConnection` instance, which you can + use to send and receive messages. + + :func:`connect` may be used as a context manager:: + + async with websockets.sync.client.connect(...) as websocket: + ... + + The connection is closed automatically when exiting the context. + + Args: + uri: URI of the WebSocket server. + sock: Preexisting TCP socket. ``sock`` overrides the host and port + from ``uri``. You may call :func:`socket.create_connection` to + create a suitable TCP socket. + ssl_context: Configuration for enabling TLS on the connection. + server_hostname: Host name for the TLS handshake. ``server_hostname`` + overrides the host name from ``uri``. + origin: Value of the ``Origin`` header, for servers that require it. + extensions: List of supported extensions, in order in which they + should be negotiated and run. + subprotocols: List of supported subprotocols, in order of decreasing + preference. + additional_headers (HeadersLike | None): Arbitrary HTTP headers to add + to the handshake request. + user_agent_header: Value of the ``User-Agent`` request header. + It defaults to ``"Python/x.y.z websockets/X.Y"``. + Setting it to :obj:`None` removes the header. + compression: The "permessage-deflate" extension is enabled by default. + Set ``compression`` to :obj:`None` to disable it. See the + :doc:`compression guide <../../topics/compression>` for details. + open_timeout: Timeout for opening the connection in seconds. + :obj:`None` disables the timeout. + close_timeout: Timeout for closing the connection in seconds. + :obj:`None` disables the timeout. + max_size: Maximum size of incoming messages in bytes. + :obj:`None` disables the limit. + logger: Logger for this client. + It defaults to ``logging.getLogger("websockets.client")``. + See the :doc:`logging guide <../../topics/logging>` for details. + create_connection: Factory for the :class:`ClientConnection` managing + the connection. Set it to a wrapper or a subclass to customize + connection handling. + + Raises: + InvalidURI: If ``uri`` isn't a valid WebSocket URI. + OSError: If the TCP connection fails. + InvalidHandshake: If the opening handshake fails. + TimeoutError: If the opening handshake times out. + + """ + + # Process parameters + + wsuri = parse_uri(uri) + if not wsuri.secure and ssl_context is not None: + raise TypeError("ssl_context argument is incompatible with a ws:// URI") + + if unix: + if path is None and sock is None: + raise TypeError("missing path argument") + elif path is not None and sock is not None: + raise TypeError("path and sock arguments are incompatible") + else: + assert path is None # private argument, only set by unix_connect() + + if subprotocols is not None: + validate_subprotocols(subprotocols) + + if compression == "deflate": + extensions = enable_client_permessage_deflate(extensions) + elif compression is not None: + raise ValueError(f"unsupported compression: {compression}") + + # Calculate timeouts on the TCP, TLS, and WebSocket handshakes. + # The TCP and TLS timeouts must be set on the socket, then removed + # to avoid conflicting with the WebSocket timeout in handshake(). + deadline = Deadline(open_timeout) + + if create_connection is None: + create_connection = ClientConnection + + try: + # Connect socket + + if sock is None: + if unix: + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.settimeout(deadline.timeout()) + assert path is not None # validated above -- this is for mpypy + sock.connect(path) + else: + sock = socket.create_connection( + (wsuri.host, wsuri.port), + deadline.timeout(), + ) + sock.settimeout(None) + + # Disable Nagle algorithm + + if not unix: + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True) + + # Initialize TLS wrapper and perform TLS handshake + + if wsuri.secure: + if ssl_context is None: + ssl_context = ssl.create_default_context() + if server_hostname is None: + server_hostname = wsuri.host + sock.settimeout(deadline.timeout()) + sock = ssl_context.wrap_socket(sock, server_hostname=server_hostname) + sock.settimeout(None) + + # Initialize WebSocket connection + + protocol = ClientProtocol( + wsuri, + origin=origin, + extensions=extensions, + subprotocols=subprotocols, + state=CONNECTING, + max_size=max_size, + logger=logger, + ) + + # Initialize WebSocket protocol + + connection = create_connection( + sock, + protocol, + close_timeout=close_timeout, + ) + # On failure, handshake() closes the socket and raises an exception. + connection.handshake( + additional_headers, + user_agent_header, + deadline.timeout(), + ) + + except Exception: + if sock is not None: + sock.close() + raise + + return connection + + +def unix_connect( + path: Optional[str] = None, + uri: Optional[str] = None, + **kwargs: Any, +) -> ClientConnection: + """ + Connect to a WebSocket server listening on a Unix socket. + + This function is identical to :func:`connect`, except for the additional + ``path`` argument. It's only available on Unix. + + It's mainly useful for debugging servers listening on Unix sockets. + + Args: + path: File system path to the Unix socket. + uri: URI of the WebSocket server. ``uri`` defaults to + ``ws://localhost/`` or, when a ``ssl_context`` is provided, to + ``wss://localhost/``. + + """ + if uri is None: + if kwargs.get("ssl_context") is None: + uri = "ws://localhost/" + else: + uri = "wss://localhost/" + return connect(uri=uri, unix=True, path=path, **kwargs) diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/sync/connection.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/sync/connection.py new file mode 100644 index 00000000000..4a8879e3705 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/sync/connection.py @@ -0,0 +1,773 @@ +from __future__ import annotations + +import contextlib +import logging +import random +import socket +import struct +import threading +import uuid +from types import TracebackType +from typing import Any, Dict, Iterable, Iterator, Mapping, Optional, Type, Union + +from ..exceptions import ConnectionClosed, ConnectionClosedOK, ProtocolError +from ..frames import DATA_OPCODES, BytesLike, CloseCode, Frame, Opcode, prepare_ctrl +from ..http11 import Request, Response +from ..protocol import CLOSED, OPEN, Event, Protocol, State +from ..typing import Data, LoggerLike, Subprotocol +from .messages import Assembler +from .utils import Deadline + + +__all__ = ["Connection"] + +logger = logging.getLogger(__name__) + + +class Connection: + """ + Threaded implementation of a WebSocket connection. + + :class:`Connection` provides APIs shared between WebSocket servers and + clients. + + You shouldn't use it directly. Instead, use + :class:`~websockets.sync.client.ClientConnection` or + :class:`~websockets.sync.server.ServerConnection`. + + """ + + recv_bufsize = 65536 + + def __init__( + self, + socket: socket.socket, + protocol: Protocol, + *, + close_timeout: Optional[float] = 10, + ) -> None: + self.socket = socket + self.protocol = protocol + self.close_timeout = close_timeout + + # Inject reference to this instance in the protocol's logger. + self.protocol.logger = logging.LoggerAdapter( + self.protocol.logger, + {"websocket": self}, + ) + + # Copy attributes from the protocol for convenience. + self.id: uuid.UUID = self.protocol.id + """Unique identifier of the connection. Useful in logs.""" + self.logger: LoggerLike = self.protocol.logger + """Logger for this connection.""" + self.debug = self.protocol.debug + + # HTTP handshake request and response. + self.request: Optional[Request] = None + """Opening handshake request.""" + self.response: Optional[Response] = None + """Opening handshake response.""" + + # Mutex serializing interactions with the protocol. + self.protocol_mutex = threading.Lock() + + # Assembler turning frames into messages and serializing reads. + self.recv_messages = Assembler() + + # Whether we are busy sending a fragmented message. + self.send_in_progress = False + + # Deadline for the closing handshake. + self.close_deadline: Optional[Deadline] = None + + # Mapping of ping IDs to pong waiters, in chronological order. + self.pings: Dict[bytes, threading.Event] = {} + + # Receiving events from the socket. + self.recv_events_thread = threading.Thread(target=self.recv_events) + self.recv_events_thread.start() + + # Exception raised in recv_events, to be chained to ConnectionClosed + # in the user thread in order to show why the TCP connection dropped. + self.recv_events_exc: Optional[BaseException] = None + + # Public attributes + + @property + def local_address(self) -> Any: + """ + Local address of the connection. + + For IPv4 connections, this is a ``(host, port)`` tuple. + + The format of the address depends on the address family. + See :meth:`~socket.socket.getsockname`. + + """ + return self.socket.getsockname() + + @property + def remote_address(self) -> Any: + """ + Remote address of the connection. + + For IPv4 connections, this is a ``(host, port)`` tuple. + + The format of the address depends on the address family. + See :meth:`~socket.socket.getpeername`. + + """ + return self.socket.getpeername() + + @property + def subprotocol(self) -> Optional[Subprotocol]: + """ + Subprotocol negotiated during the opening handshake. + + :obj:`None` if no subprotocol was negotiated. + + """ + return self.protocol.subprotocol + + # Public methods + + def __enter__(self) -> Connection: + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + if exc_type is None: + self.close() + else: + self.close(CloseCode.INTERNAL_ERROR) + + def __iter__(self) -> Iterator[Data]: + """ + Iterate on incoming messages. + + The iterator calls :meth:`recv` and yields messages in an infinite loop. + + It exits when the connection is closed normally. It raises a + :exc:`~websockets.exceptions.ConnectionClosedError` exception after a + protocol error or a network failure. + + """ + try: + while True: + yield self.recv() + except ConnectionClosedOK: + return + + def recv(self, timeout: Optional[float] = None) -> Data: + """ + Receive the next message. + + When the connection is closed, :meth:`recv` raises + :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it raises + :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal closure + and :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol + error or a network failure. This is how you detect the end of the + message stream. + + If ``timeout`` is :obj:`None`, block until a message is received. If + ``timeout`` is set and no message is received within ``timeout`` + seconds, raise :exc:`TimeoutError`. Set ``timeout`` to ``0`` to check if + a message was already received. + + If the message is fragmented, wait until all fragments are received, + reassemble them, and return the whole message. + + Returns: + A string (:class:`str`) for a Text_ frame or a bytestring + (:class:`bytes`) for a Binary_ frame. + + .. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + .. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + + Raises: + ConnectionClosed: When the connection is closed. + RuntimeError: If two threads call :meth:`recv` or + :meth:`recv_streaming` concurrently. + + """ + try: + return self.recv_messages.get(timeout) + except EOFError: + raise self.protocol.close_exc from self.recv_events_exc + except RuntimeError: + raise RuntimeError( + "cannot call recv while another thread " + "is already running recv or recv_streaming" + ) from None + + def recv_streaming(self) -> Iterator[Data]: + """ + Receive the next message frame by frame. + + If the message is fragmented, yield each fragment as it is received. + The iterator must be fully consumed, or else the connection will become + unusable. + + :meth:`recv_streaming` raises the same exceptions as :meth:`recv`. + + Returns: + An iterator of strings (:class:`str`) for a Text_ frame or + bytestrings (:class:`bytes`) for a Binary_ frame. + + .. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + .. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + + Raises: + ConnectionClosed: When the connection is closed. + RuntimeError: If two threads call :meth:`recv` or + :meth:`recv_streaming` concurrently. + + """ + try: + yield from self.recv_messages.get_iter() + except EOFError: + raise self.protocol.close_exc from self.recv_events_exc + except RuntimeError: + raise RuntimeError( + "cannot call recv_streaming while another thread " + "is already running recv or recv_streaming" + ) from None + + def send(self, message: Union[Data, Iterable[Data]]) -> None: + """ + Send a message. + + A string (:class:`str`) is sent as a Text_ frame. A bytestring or + bytes-like object (:class:`bytes`, :class:`bytearray`, or + :class:`memoryview`) is sent as a Binary_ frame. + + .. _Text: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + .. _Binary: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.6 + + :meth:`send` also accepts an iterable of strings, bytestrings, or + bytes-like objects to enable fragmentation_. Each item is treated as a + message fragment and sent in its own frame. All items must be of the + same type, or else :meth:`send` will raise a :exc:`TypeError` and the + connection will be closed. + + .. _fragmentation: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.4 + + :meth:`send` rejects dict-like objects because this is often an error. + (If you really want to send the keys of a dict-like object as fragments, + call its :meth:`~dict.keys` method and pass the result to :meth:`send`.) + + When the connection is closed, :meth:`send` raises + :exc:`~websockets.exceptions.ConnectionClosed`. Specifically, it + raises :exc:`~websockets.exceptions.ConnectionClosedOK` after a normal + connection closure and + :exc:`~websockets.exceptions.ConnectionClosedError` after a protocol + error or a network failure. + + Args: + message: Message to send. + + Raises: + ConnectionClosed: When the connection is closed. + RuntimeError: If a connection is busy sending a fragmented message. + TypeError: If ``message`` doesn't have a supported type. + + """ + # Unfragmented message -- this case must be handled first because + # strings and bytes-like objects are iterable. + + if isinstance(message, str): + with self.send_context(): + if self.send_in_progress: + raise RuntimeError( + "cannot call send while another thread " + "is already running send" + ) + self.protocol.send_text(message.encode("utf-8")) + + elif isinstance(message, BytesLike): + with self.send_context(): + if self.send_in_progress: + raise RuntimeError( + "cannot call send while another thread " + "is already running send" + ) + self.protocol.send_binary(message) + + # Catch a common mistake -- passing a dict to send(). + + elif isinstance(message, Mapping): + raise TypeError("data is a dict-like object") + + # Fragmented message -- regular iterator. + + elif isinstance(message, Iterable): + chunks = iter(message) + try: + chunk = next(chunks) + except StopIteration: + return + + try: + # First fragment. + if isinstance(chunk, str): + text = True + with self.send_context(): + if self.send_in_progress: + raise RuntimeError( + "cannot call send while another thread " + "is already running send" + ) + self.send_in_progress = True + self.protocol.send_text( + chunk.encode("utf-8"), + fin=False, + ) + elif isinstance(chunk, BytesLike): + text = False + with self.send_context(): + if self.send_in_progress: + raise RuntimeError( + "cannot call send while another thread " + "is already running send" + ) + self.send_in_progress = True + self.protocol.send_binary( + chunk, + fin=False, + ) + else: + raise TypeError("data iterable must contain bytes or str") + + # Other fragments + for chunk in chunks: + if isinstance(chunk, str) and text: + with self.send_context(): + assert self.send_in_progress + self.protocol.send_continuation( + chunk.encode("utf-8"), + fin=False, + ) + elif isinstance(chunk, BytesLike) and not text: + with self.send_context(): + assert self.send_in_progress + self.protocol.send_continuation( + chunk, + fin=False, + ) + else: + raise TypeError("data iterable must contain uniform types") + + # Final fragment. + with self.send_context(): + self.protocol.send_continuation(b"", fin=True) + self.send_in_progress = False + + except RuntimeError: + # We didn't start sending a fragmented message. + raise + + except Exception: + # We're half-way through a fragmented message and we can't + # complete it. This makes the connection unusable. + with self.send_context(): + self.protocol.fail( + CloseCode.INTERNAL_ERROR, + "error in fragmented message", + ) + raise + + else: + raise TypeError("data must be bytes, str, or iterable") + + def close(self, code: int = CloseCode.NORMAL_CLOSURE, reason: str = "") -> None: + """ + Perform the closing handshake. + + :meth:`close` waits for the other end to complete the handshake, for the + TCP connection to terminate, and for all incoming messages to be read + with :meth:`recv`. + + :meth:`close` is idempotent: it doesn't do anything once the + connection is closed. + + Args: + code: WebSocket close code. + reason: WebSocket close reason. + + """ + try: + # The context manager takes care of waiting for the TCP connection + # to terminate after calling a method that sends a close frame. + with self.send_context(): + if self.send_in_progress: + self.protocol.fail( + CloseCode.INTERNAL_ERROR, + "close during fragmented message", + ) + else: + self.protocol.send_close(code, reason) + except ConnectionClosed: + # Ignore ConnectionClosed exceptions raised from send_context(). + # They mean that the connection is closed, which was the goal. + pass + + def ping(self, data: Optional[Data] = None) -> threading.Event: + """ + Send a Ping_. + + .. _Ping: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.2 + + A ping may serve as a keepalive or as a check that the remote endpoint + received all messages up to this point + + Args: + data: Payload of the ping. A :class:`str` will be encoded to UTF-8. + If ``data`` is :obj:`None`, the payload is four random bytes. + + Returns: + An event that will be set when the corresponding pong is received. + You can ignore it if you don't intend to wait. + + :: + + pong_event = ws.ping() + pong_event.wait() # only if you want to wait for the pong + + Raises: + ConnectionClosed: When the connection is closed. + RuntimeError: If another ping was sent with the same data and + the corresponding pong wasn't received yet. + + """ + if data is not None: + data = prepare_ctrl(data) + + with self.send_context(): + # Protect against duplicates if a payload is explicitly set. + if data in self.pings: + raise RuntimeError("already waiting for a pong with the same data") + + # Generate a unique random payload otherwise. + while data is None or data in self.pings: + data = struct.pack("!I", random.getrandbits(32)) + + pong_waiter = threading.Event() + self.pings[data] = pong_waiter + self.protocol.send_ping(data) + return pong_waiter + + def pong(self, data: Data = b"") -> None: + """ + Send a Pong_. + + .. _Pong: https://www.rfc-editor.org/rfc/rfc6455.html#section-5.5.3 + + An unsolicited pong may serve as a unidirectional heartbeat. + + Args: + data: Payload of the pong. A :class:`str` will be encoded to UTF-8. + + Raises: + ConnectionClosed: When the connection is closed. + + """ + data = prepare_ctrl(data) + + with self.send_context(): + self.protocol.send_pong(data) + + # Private methods + + def process_event(self, event: Event) -> None: + """ + Process one incoming event. + + This method is overridden in subclasses to handle the handshake. + + """ + assert isinstance(event, Frame) + if event.opcode in DATA_OPCODES: + self.recv_messages.put(event) + + if event.opcode is Opcode.PONG: + self.acknowledge_pings(bytes(event.data)) + + def acknowledge_pings(self, data: bytes) -> None: + """ + Acknowledge pings when receiving a pong. + + """ + with self.protocol_mutex: + # Ignore unsolicited pong. + if data not in self.pings: + return + # Sending a pong for only the most recent ping is legal. + # Acknowledge all previous pings too in that case. + ping_id = None + ping_ids = [] + for ping_id, ping in self.pings.items(): + ping_ids.append(ping_id) + ping.set() + if ping_id == data: + break + else: + raise AssertionError("solicited pong not found in pings") + # Remove acknowledged pings from self.pings. + for ping_id in ping_ids: + del self.pings[ping_id] + + def recv_events(self) -> None: + """ + Read incoming data from the socket and process events. + + Run this method in a thread as long as the connection is alive. + + ``recv_events()`` exits immediately when the ``self.socket`` is closed. + + """ + try: + while True: + try: + if self.close_deadline is not None: + self.socket.settimeout(self.close_deadline.timeout()) + data = self.socket.recv(self.recv_bufsize) + except Exception as exc: + if self.debug: + self.logger.debug("error while receiving data", exc_info=True) + # When the closing handshake is initiated by our side, + # recv() may block until send_context() closes the socket. + # In that case, send_context() already set recv_events_exc. + # Calling set_recv_events_exc() avoids overwriting it. + with self.protocol_mutex: + self.set_recv_events_exc(exc) + break + + if data == b"": + break + + # Acquire the connection lock. + with self.protocol_mutex: + # Feed incoming data to the connection. + self.protocol.receive_data(data) + + # This isn't expected to raise an exception. + events = self.protocol.events_received() + + # Write outgoing data to the socket. + try: + self.send_data() + except Exception as exc: + if self.debug: + self.logger.debug("error while sending data", exc_info=True) + # Similarly to the above, avoid overriding an exception + # set by send_context(), in case of a race condition + # i.e. send_context() closes the socket after recv() + # returns above but before send_data() calls send(). + self.set_recv_events_exc(exc) + break + + if self.protocol.close_expected(): + # If the connection is expected to close soon, set the + # close deadline based on the close timeout. + if self.close_deadline is None: + self.close_deadline = Deadline(self.close_timeout) + + # Unlock conn_mutex before processing events. Else, the + # application can't send messages in response to events. + + # If self.send_data raised an exception, then events are lost. + # Given that automatic responses write small amounts of data, + # this should be uncommon, so we don't handle the edge case. + + try: + for event in events: + # This may raise EOFError if the closing handshake + # times out while a message is waiting to be read. + self.process_event(event) + except EOFError: + break + + # Breaking out of the while True: ... loop means that we believe + # that the socket doesn't work anymore. + with self.protocol_mutex: + # Feed the end of the data stream to the connection. + self.protocol.receive_eof() + + # This isn't expected to generate events. + assert not self.protocol.events_received() + + # There is no error handling because send_data() can only write + # the end of the data stream here and it handles errors itself. + self.send_data() + + except Exception as exc: + # This branch should never run. It's a safety net in case of bugs. + self.logger.error("unexpected internal error", exc_info=True) + with self.protocol_mutex: + self.set_recv_events_exc(exc) + # We don't know where we crashed. Force protocol state to CLOSED. + self.protocol.state = CLOSED + finally: + # This isn't expected to raise an exception. + self.close_socket() + + @contextlib.contextmanager + def send_context( + self, + *, + expected_state: State = OPEN, # CONNECTING during the opening handshake + ) -> Iterator[None]: + """ + Create a context for writing to the connection from user code. + + On entry, :meth:`send_context` acquires the connection lock and checks + that the connection is open; on exit, it writes outgoing data to the + socket:: + + with self.send_context(): + self.protocol.send_text(message.encode("utf-8")) + + When the connection isn't open on entry, when the connection is expected + to close on exit, or when an unexpected error happens, terminating the + connection, :meth:`send_context` waits until the connection is closed + then raises :exc:`~websockets.exceptions.ConnectionClosed`. + + """ + # Should we wait until the connection is closed? + wait_for_close = False + # Should we close the socket and raise ConnectionClosed? + raise_close_exc = False + # What exception should we chain ConnectionClosed to? + original_exc: Optional[BaseException] = None + + # Acquire the protocol lock. + with self.protocol_mutex: + if self.protocol.state is expected_state: + # Let the caller interact with the protocol. + try: + yield + except (ProtocolError, RuntimeError): + # The protocol state wasn't changed. Exit immediately. + raise + except Exception as exc: + self.logger.error("unexpected internal error", exc_info=True) + # This branch should never run. It's a safety net in case of + # bugs. Since we don't know what happened, we will close the + # connection and raise the exception to the caller. + wait_for_close = False + raise_close_exc = True + original_exc = exc + else: + # Check if the connection is expected to close soon. + if self.protocol.close_expected(): + wait_for_close = True + # If the connection is expected to close soon, set the + # close deadline based on the close timeout. + + # Since we tested earlier that protocol.state was OPEN + # (or CONNECTING) and we didn't release protocol_mutex, + # it is certain that self.close_deadline is still None. + assert self.close_deadline is None + self.close_deadline = Deadline(self.close_timeout) + # Write outgoing data to the socket. + try: + self.send_data() + except Exception as exc: + if self.debug: + self.logger.debug("error while sending data", exc_info=True) + # While the only expected exception here is OSError, + # other exceptions would be treated identically. + wait_for_close = False + raise_close_exc = True + original_exc = exc + + else: # self.protocol.state is not expected_state + # Minor layering violation: we assume that the connection + # will be closing soon if it isn't in the expected state. + wait_for_close = True + raise_close_exc = True + + # To avoid a deadlock, release the connection lock by exiting the + # context manager before waiting for recv_events() to terminate. + + # If the connection is expected to close soon and the close timeout + # elapses, close the socket to terminate the connection. + if wait_for_close: + if self.close_deadline is None: + timeout = self.close_timeout + else: + # Thread.join() returns immediately if timeout is negative. + timeout = self.close_deadline.timeout(raise_if_elapsed=False) + self.recv_events_thread.join(timeout) + + if self.recv_events_thread.is_alive(): + # There's no risk to overwrite another error because + # original_exc is never set when wait_for_close is True. + assert original_exc is None + original_exc = TimeoutError("timed out while closing connection") + # Set recv_events_exc before closing the socket in order to get + # proper exception reporting. + raise_close_exc = True + with self.protocol_mutex: + self.set_recv_events_exc(original_exc) + + # If an error occurred, close the socket to terminate the connection and + # raise an exception. + if raise_close_exc: + self.close_socket() + self.recv_events_thread.join() + raise self.protocol.close_exc from original_exc + + def send_data(self) -> None: + """ + Send outgoing data. + + This method requires holding protocol_mutex. + + Raises: + OSError: When a socket operations fails. + + """ + assert self.protocol_mutex.locked() + for data in self.protocol.data_to_send(): + if data: + if self.close_deadline is not None: + self.socket.settimeout(self.close_deadline.timeout()) + self.socket.sendall(data) + else: + try: + self.socket.shutdown(socket.SHUT_WR) + except OSError: # socket already closed + pass + + def set_recv_events_exc(self, exc: Optional[BaseException]) -> None: + """ + Set recv_events_exc, if not set yet. + + This method requires holding protocol_mutex. + + """ + assert self.protocol_mutex.locked() + if self.recv_events_exc is None: + self.recv_events_exc = exc + + def close_socket(self) -> None: + """ + Shutdown and close socket. Close message assembler. + + Calling close_socket() guarantees that recv_events() terminates. Indeed, + recv_events() may block only on socket.recv() or on recv_messages.put(). + + """ + # shutdown() is required to interrupt recv() on Linux. + try: + self.socket.shutdown(socket.SHUT_RDWR) + except OSError: + pass # socket is already closed + self.socket.close() + self.recv_messages.close() diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/sync/messages.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/sync/messages.py new file mode 100644 index 00000000000..67a22313ca1 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/sync/messages.py @@ -0,0 +1,281 @@ +from __future__ import annotations + +import codecs +import queue +import threading +from typing import Iterator, List, Optional, cast + +from ..frames import Frame, Opcode +from ..typing import Data + + +__all__ = ["Assembler"] + +UTF8Decoder = codecs.getincrementaldecoder("utf-8") + + +class Assembler: + """ + Assemble messages from frames. + + """ + + def __init__(self) -> None: + # Serialize reads and writes -- except for reads via synchronization + # primitives provided by the threading and queue modules. + self.mutex = threading.Lock() + + # We create a latch with two events to ensure proper interleaving of + # writing and reading messages. + # put() sets this event to tell get() that a message can be fetched. + self.message_complete = threading.Event() + # get() sets this event to let put() that the message was fetched. + self.message_fetched = threading.Event() + + # This flag prevents concurrent calls to get() by user code. + self.get_in_progress = False + # This flag prevents concurrent calls to put() by library code. + self.put_in_progress = False + + # Decoder for text frames, None for binary frames. + self.decoder: Optional[codecs.IncrementalDecoder] = None + + # Buffer of frames belonging to the same message. + self.chunks: List[Data] = [] + + # When switching from "buffering" to "streaming", we use a thread-safe + # queue for transferring frames from the writing thread (library code) + # to the reading thread (user code). We're buffering when chunks_queue + # is None and streaming when it's a SimpleQueue. None is a sentinel + # value marking the end of the stream, superseding message_complete. + + # Stream data from frames belonging to the same message. + # Remove quotes around type when dropping Python < 3.9. + self.chunks_queue: Optional["queue.SimpleQueue[Optional[Data]]"] = None + + # This flag marks the end of the stream. + self.closed = False + + def get(self, timeout: Optional[float] = None) -> Data: + """ + Read the next message. + + :meth:`get` returns a single :class:`str` or :class:`bytes`. + + If the message is fragmented, :meth:`get` waits until the last frame is + received, then it reassembles the message and returns it. To receive + messages frame by frame, use :meth:`get_iter` instead. + + Args: + timeout: If a timeout is provided and elapses before a complete + message is received, :meth:`get` raises :exc:`TimeoutError`. + + Raises: + EOFError: If the stream of frames has ended. + RuntimeError: If two threads run :meth:`get` or :meth:``get_iter` + concurrently. + + """ + with self.mutex: + if self.closed: + raise EOFError("stream of frames ended") + + if self.get_in_progress: + raise RuntimeError("get or get_iter is already running") + + self.get_in_progress = True + + # If the message_complete event isn't set yet, release the lock to + # allow put() to run and eventually set it. + # Locking with get_in_progress ensures only one thread can get here. + completed = self.message_complete.wait(timeout) + + with self.mutex: + self.get_in_progress = False + + # Waiting for a complete message timed out. + if not completed: + raise TimeoutError(f"timed out in {timeout:.1f}s") + + # get() was unblocked by close() rather than put(). + if self.closed: + raise EOFError("stream of frames ended") + + assert self.message_complete.is_set() + self.message_complete.clear() + + joiner: Data = b"" if self.decoder is None else "" + # mypy cannot figure out that chunks have the proper type. + message: Data = joiner.join(self.chunks) # type: ignore + + assert not self.message_fetched.is_set() + self.message_fetched.set() + + self.chunks = [] + assert self.chunks_queue is None + + return message + + def get_iter(self) -> Iterator[Data]: + """ + Stream the next message. + + Iterating the return value of :meth:`get_iter` yields a :class:`str` or + :class:`bytes` for each frame in the message. + + The iterator must be fully consumed before calling :meth:`get_iter` or + :meth:`get` again. Else, :exc:`RuntimeError` is raised. + + This method only makes sense for fragmented messages. If messages aren't + fragmented, use :meth:`get` instead. + + Raises: + EOFError: If the stream of frames has ended. + RuntimeError: If two threads run :meth:`get` or :meth:``get_iter` + concurrently. + + """ + with self.mutex: + if self.closed: + raise EOFError("stream of frames ended") + + if self.get_in_progress: + raise RuntimeError("get or get_iter is already running") + + chunks = self.chunks + self.chunks = [] + self.chunks_queue = cast( + # Remove quotes around type when dropping Python < 3.9. + "queue.SimpleQueue[Optional[Data]]", + queue.SimpleQueue(), + ) + + # Sending None in chunk_queue supersedes setting message_complete + # when switching to "streaming". If message is already complete + # when the switch happens, put() didn't send None, so we have to. + if self.message_complete.is_set(): + self.chunks_queue.put(None) + + self.get_in_progress = True + + # Locking with get_in_progress ensures only one thread can get here. + yield from chunks + while True: + chunk = self.chunks_queue.get() + if chunk is None: + break + yield chunk + + with self.mutex: + self.get_in_progress = False + + assert self.message_complete.is_set() + self.message_complete.clear() + + # get_iter() was unblocked by close() rather than put(). + if self.closed: + raise EOFError("stream of frames ended") + + assert not self.message_fetched.is_set() + self.message_fetched.set() + + assert self.chunks == [] + self.chunks_queue = None + + def put(self, frame: Frame) -> None: + """ + Add ``frame`` to the next message. + + When ``frame`` is the final frame in a message, :meth:`put` waits until + the message is fetched, either by calling :meth:`get` or by fully + consuming the return value of :meth:`get_iter`. + + :meth:`put` assumes that the stream of frames respects the protocol. If + it doesn't, the behavior is undefined. + + Raises: + EOFError: If the stream of frames has ended. + RuntimeError: If two threads run :meth:`put` concurrently. + + """ + with self.mutex: + if self.closed: + raise EOFError("stream of frames ended") + + if self.put_in_progress: + raise RuntimeError("put is already running") + + if frame.opcode is Opcode.TEXT: + self.decoder = UTF8Decoder(errors="strict") + elif frame.opcode is Opcode.BINARY: + self.decoder = None + elif frame.opcode is Opcode.CONT: + pass + else: + # Ignore control frames. + return + + data: Data + if self.decoder is not None: + data = self.decoder.decode(frame.data, frame.fin) + else: + data = frame.data + + if self.chunks_queue is None: + self.chunks.append(data) + else: + self.chunks_queue.put(data) + + if not frame.fin: + return + + # Message is complete. Wait until it's fetched to return. + + assert not self.message_complete.is_set() + self.message_complete.set() + + if self.chunks_queue is not None: + self.chunks_queue.put(None) + + assert not self.message_fetched.is_set() + + self.put_in_progress = True + + # Release the lock to allow get() to run and eventually set the event. + self.message_fetched.wait() + + with self.mutex: + self.put_in_progress = False + + assert self.message_fetched.is_set() + self.message_fetched.clear() + + # put() was unblocked by close() rather than get() or get_iter(). + if self.closed: + raise EOFError("stream of frames ended") + + self.decoder = None + + def close(self) -> None: + """ + End the stream of frames. + + Callling :meth:`close` concurrently with :meth:`get`, :meth:`get_iter`, + or :meth:`put` is safe. They will raise :exc:`EOFError`. + + """ + with self.mutex: + if self.closed: + return + + self.closed = True + + # Unblock get or get_iter. + if self.get_in_progress: + self.message_complete.set() + if self.chunks_queue is not None: + self.chunks_queue.put(None) + + # Unblock put(). + if self.put_in_progress: + self.message_fetched.set() diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/sync/server.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/sync/server.py new file mode 100644 index 00000000000..14767968c97 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/sync/server.py @@ -0,0 +1,530 @@ +from __future__ import annotations + +import http +import logging +import os +import selectors +import socket +import ssl +import sys +import threading +from types import TracebackType +from typing import Any, Callable, Optional, Sequence, Type + +from websockets.frames import CloseCode + +from ..extensions.base import ServerExtensionFactory +from ..extensions.permessage_deflate import enable_server_permessage_deflate +from ..headers import validate_subprotocols +from ..http import USER_AGENT +from ..http11 import Request, Response +from ..protocol import CONNECTING, OPEN, Event +from ..server import ServerProtocol +from ..typing import LoggerLike, Origin, Subprotocol +from .connection import Connection +from .utils import Deadline + + +__all__ = ["serve", "unix_serve", "ServerConnection", "WebSocketServer"] + + +class ServerConnection(Connection): + """ + Threaded implementation of a WebSocket server connection. + + :class:`ServerConnection` provides :meth:`recv` and :meth:`send` methods for + receiving and sending messages. + + It supports iteration to receive messages:: + + for message in websocket: + process(message) + + The iterator exits normally when the connection is closed with close code + 1000 (OK) or 1001 (going away) or without a close code. It raises a + :exc:`~websockets.exceptions.ConnectionClosedError` when the connection is + closed with any other code. + + Args: + socket: Socket connected to a WebSocket client. + protocol: Sans-I/O connection. + close_timeout: Timeout for closing the connection in seconds. + + """ + + def __init__( + self, + socket: socket.socket, + protocol: ServerProtocol, + *, + close_timeout: Optional[float] = 10, + ) -> None: + self.protocol: ServerProtocol + self.request_rcvd = threading.Event() + super().__init__( + socket, + protocol, + close_timeout=close_timeout, + ) + + def handshake( + self, + process_request: Optional[ + Callable[ + [ServerConnection, Request], + Optional[Response], + ] + ] = None, + process_response: Optional[ + Callable[ + [ServerConnection, Request, Response], + Optional[Response], + ] + ] = None, + server_header: Optional[str] = USER_AGENT, + timeout: Optional[float] = None, + ) -> None: + """ + Perform the opening handshake. + + """ + if not self.request_rcvd.wait(timeout): + self.close_socket() + self.recv_events_thread.join() + raise TimeoutError("timed out during handshake") + + if self.request is None: + self.close_socket() + self.recv_events_thread.join() + raise ConnectionError("connection closed during handshake") + + with self.send_context(expected_state=CONNECTING): + self.response = None + + if process_request is not None: + try: + self.response = process_request(self, self.request) + except Exception as exc: + self.protocol.handshake_exc = exc + self.logger.error("opening handshake failed", exc_info=True) + self.response = self.protocol.reject( + http.HTTPStatus.INTERNAL_SERVER_ERROR, + ( + "Failed to open a WebSocket connection.\n" + "See server log for more information.\n" + ), + ) + + if self.response is None: + self.response = self.protocol.accept(self.request) + + if server_header is not None: + self.response.headers["Server"] = server_header + + if process_response is not None: + try: + response = process_response(self, self.request, self.response) + except Exception as exc: + self.protocol.handshake_exc = exc + self.logger.error("opening handshake failed", exc_info=True) + self.response = self.protocol.reject( + http.HTTPStatus.INTERNAL_SERVER_ERROR, + ( + "Failed to open a WebSocket connection.\n" + "See server log for more information.\n" + ), + ) + else: + if response is not None: + self.response = response + + self.protocol.send_response(self.response) + + if self.protocol.state is not OPEN: + self.recv_events_thread.join(self.close_timeout) + self.close_socket() + self.recv_events_thread.join() + + if self.protocol.handshake_exc is not None: + raise self.protocol.handshake_exc + + def process_event(self, event: Event) -> None: + """ + Process one incoming event. + + """ + # First event - handshake request. + if self.request is None: + assert isinstance(event, Request) + self.request = event + self.request_rcvd.set() + # Later events - frames. + else: + super().process_event(event) + + def recv_events(self) -> None: + """ + Read incoming data from the socket and process events. + + """ + try: + super().recv_events() + finally: + # If the connection is closed during the handshake, unblock it. + self.request_rcvd.set() + + +class WebSocketServer: + """ + WebSocket server returned by :func:`serve`. + + This class mirrors the API of :class:`~socketserver.BaseServer`, notably the + :meth:`~socketserver.BaseServer.serve_forever` and + :meth:`~socketserver.BaseServer.shutdown` methods, as well as the context + manager protocol. + + Args: + socket: Server socket listening for new connections. + handler: Handler for one connection. Receives the socket and address + returned by :meth:`~socket.socket.accept`. + logger: Logger for this server. + + """ + + def __init__( + self, + socket: socket.socket, + handler: Callable[[socket.socket, Any], None], + logger: Optional[LoggerLike] = None, + ): + self.socket = socket + self.handler = handler + if logger is None: + logger = logging.getLogger("websockets.server") + self.logger = logger + if sys.platform != "win32": + self.shutdown_watcher, self.shutdown_notifier = os.pipe() + + def serve_forever(self) -> None: + """ + See :meth:`socketserver.BaseServer.serve_forever`. + + This method doesn't return. Calling :meth:`shutdown` from another thread + stops the server. + + Typical use:: + + with serve(...) as server: + server.serve_forever() + + """ + poller = selectors.DefaultSelector() + poller.register(self.socket, selectors.EVENT_READ) + if sys.platform != "win32": + poller.register(self.shutdown_watcher, selectors.EVENT_READ) + + while True: + poller.select() + try: + # If the socket is closed, this will raise an exception and exit + # the loop. So we don't need to check the return value of select(). + sock, addr = self.socket.accept() + except OSError: + break + thread = threading.Thread(target=self.handler, args=(sock, addr)) + thread.start() + + def shutdown(self) -> None: + """ + See :meth:`socketserver.BaseServer.shutdown`. + + """ + self.socket.close() + if sys.platform != "win32": + os.write(self.shutdown_notifier, b"x") + + def fileno(self) -> int: + """ + See :meth:`socketserver.BaseServer.fileno`. + + """ + return self.socket.fileno() + + def __enter__(self) -> WebSocketServer: + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + self.shutdown() + + +def serve( + handler: Callable[[ServerConnection], None], + host: Optional[str] = None, + port: Optional[int] = None, + *, + # TCP/TLS — unix and path are only for unix_serve() + sock: Optional[socket.socket] = None, + ssl_context: Optional[ssl.SSLContext] = None, + unix: bool = False, + path: Optional[str] = None, + # WebSocket + origins: Optional[Sequence[Optional[Origin]]] = None, + extensions: Optional[Sequence[ServerExtensionFactory]] = None, + subprotocols: Optional[Sequence[Subprotocol]] = None, + select_subprotocol: Optional[ + Callable[ + [ServerConnection, Sequence[Subprotocol]], + Optional[Subprotocol], + ] + ] = None, + process_request: Optional[ + Callable[ + [ServerConnection, Request], + Optional[Response], + ] + ] = None, + process_response: Optional[ + Callable[ + [ServerConnection, Request, Response], + Optional[Response], + ] + ] = None, + server_header: Optional[str] = USER_AGENT, + compression: Optional[str] = "deflate", + # Timeouts + open_timeout: Optional[float] = 10, + close_timeout: Optional[float] = 10, + # Limits + max_size: Optional[int] = 2**20, + # Logging + logger: Optional[LoggerLike] = None, + # Escape hatch for advanced customization + create_connection: Optional[Type[ServerConnection]] = None, +) -> WebSocketServer: + """ + Create a WebSocket server listening on ``host`` and ``port``. + + Whenever a client connects, the server creates a :class:`ServerConnection`, + performs the opening handshake, and delegates to the ``handler``. + + The handler receives a :class:`ServerConnection` instance, which you can use + to send and receive messages. + + Once the handler completes, either normally or with an exception, the server + performs the closing handshake and closes the connection. + + :class:`WebSocketServer` mirrors the API of + :class:`~socketserver.BaseServer`. Treat it as a context manager to ensure + that it will be closed and call the :meth:`~WebSocketServer.serve_forever` + method to serve requests:: + + def handler(websocket): + ... + + with websockets.sync.server.serve(handler, ...) as server: + server.serve_forever() + + Args: + handler: Connection handler. It receives the WebSocket connection, + which is a :class:`ServerConnection`, in argument. + host: Network interfaces the server binds to. + See :func:`~socket.create_server` for details. + port: TCP port the server listens on. + See :func:`~socket.create_server` for details. + sock: Preexisting TCP socket. ``sock`` replaces ``host`` and ``port``. + You may call :func:`socket.create_server` to create a suitable TCP + socket. + ssl_context: Configuration for enabling TLS on the connection. + origins: Acceptable values of the ``Origin`` header, for defending + against Cross-Site WebSocket Hijacking attacks. Include :obj:`None` + in the list if the lack of an origin is acceptable. + extensions: List of supported extensions, in order in which they + should be negotiated and run. + subprotocols: List of supported subprotocols, in order of decreasing + preference. + select_subprotocol: Callback for selecting a subprotocol among + those supported by the client and the server. It receives a + :class:`ServerConnection` (not a + :class:`~websockets.server.ServerProtocol`!) instance and a list of + subprotocols offered by the client. Other than the first argument, + it has the same behavior as the + :meth:`ServerProtocol.select_subprotocol + <websockets.server.ServerProtocol.select_subprotocol>` method. + process_request: Intercept the request during the opening handshake. + Return an HTTP response to force the response or :obj:`None` to + continue normally. When you force an HTTP 101 Continue response, + the handshake is successful. Else, the connection is aborted. + process_response: Intercept the response during the opening handshake. + Return an HTTP response to force the response or :obj:`None` to + continue normally. When you force an HTTP 101 Continue response, + the handshake is successful. Else, the connection is aborted. + server_header: Value of the ``Server`` response header. + It defaults to ``"Python/x.y.z websockets/X.Y"``. Setting it to + :obj:`None` removes the header. + compression: The "permessage-deflate" extension is enabled by default. + Set ``compression`` to :obj:`None` to disable it. See the + :doc:`compression guide <../../topics/compression>` for details. + open_timeout: Timeout for opening connections in seconds. + :obj:`None` disables the timeout. + close_timeout: Timeout for closing connections in seconds. + :obj:`None` disables the timeout. + max_size: Maximum size of incoming messages in bytes. + :obj:`None` disables the limit. + logger: Logger for this server. + It defaults to ``logging.getLogger("websockets.server")``. See the + :doc:`logging guide <../../topics/logging>` for details. + create_connection: Factory for the :class:`ServerConnection` managing + the connection. Set it to a wrapper or a subclass to customize + connection handling. + """ + + # Process parameters + + if subprotocols is not None: + validate_subprotocols(subprotocols) + + if compression == "deflate": + extensions = enable_server_permessage_deflate(extensions) + elif compression is not None: + raise ValueError(f"unsupported compression: {compression}") + + if create_connection is None: + create_connection = ServerConnection + + # Bind socket and listen + + if sock is None: + if unix: + if path is None: + raise TypeError("missing path argument") + sock = socket.create_server(path, family=socket.AF_UNIX) + else: + sock = socket.create_server((host, port)) + else: + if path is not None: + raise TypeError("path and sock arguments are incompatible") + + # Initialize TLS wrapper + + if ssl_context is not None: + sock = ssl_context.wrap_socket( + sock, + server_side=True, + # Delay TLS handshake until after we set a timeout on the socket. + do_handshake_on_connect=False, + ) + + # Define request handler + + def conn_handler(sock: socket.socket, addr: Any) -> None: + # Calculate timeouts on the TLS and WebSocket handshakes. + # The TLS timeout must be set on the socket, then removed + # to avoid conflicting with the WebSocket timeout in handshake(). + deadline = Deadline(open_timeout) + + try: + # Disable Nagle algorithm + + if not unix: + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True) + + # Perform TLS handshake + + if ssl_context is not None: + sock.settimeout(deadline.timeout()) + assert isinstance(sock, ssl.SSLSocket) # mypy cannot figure this out + sock.do_handshake() + sock.settimeout(None) + + # Create a closure so that select_subprotocol has access to self. + + protocol_select_subprotocol: Optional[ + Callable[ + [ServerProtocol, Sequence[Subprotocol]], + Optional[Subprotocol], + ] + ] = None + + if select_subprotocol is not None: + + def protocol_select_subprotocol( + protocol: ServerProtocol, + subprotocols: Sequence[Subprotocol], + ) -> Optional[Subprotocol]: + # mypy doesn't know that select_subprotocol is immutable. + assert select_subprotocol is not None + # Ensure this function is only used in the intended context. + assert protocol is connection.protocol + return select_subprotocol(connection, subprotocols) + + # Initialize WebSocket connection + + protocol = ServerProtocol( + origins=origins, + extensions=extensions, + subprotocols=subprotocols, + select_subprotocol=protocol_select_subprotocol, + state=CONNECTING, + max_size=max_size, + logger=logger, + ) + + # Initialize WebSocket protocol + + assert create_connection is not None # help mypy + connection = create_connection( + sock, + protocol, + close_timeout=close_timeout, + ) + # On failure, handshake() closes the socket, raises an exception, and + # logs it. + connection.handshake( + process_request, + process_response, + server_header, + deadline.timeout(), + ) + + except Exception: + sock.close() + return + + try: + handler(connection) + except Exception: + protocol.logger.error("connection handler failed", exc_info=True) + connection.close(CloseCode.INTERNAL_ERROR) + else: + connection.close() + + # Initialize server + + return WebSocketServer(sock, conn_handler, logger) + + +def unix_serve( + handler: Callable[[ServerConnection], Any], + path: Optional[str] = None, + **kwargs: Any, +) -> WebSocketServer: + """ + Create a WebSocket server listening on a Unix socket. + + This function is identical to :func:`serve`, except the ``host`` and + ``port`` arguments are replaced by ``path``. It's only available on Unix. + + It's useful for deploying a server behind a reverse proxy such as nginx. + + Args: + handler: Connection handler. It receives the WebSocket connection, + which is a :class:`ServerConnection`, in argument. + path: File system path to the Unix socket. + + """ + return serve(handler, path=path, unix=True, **kwargs) diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/sync/utils.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/sync/utils.py new file mode 100644 index 00000000000..471f32e19d4 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/sync/utils.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +import time +from typing import Optional + + +__all__ = ["Deadline"] + + +class Deadline: + """ + Manage timeouts across multiple steps. + + Args: + timeout: Time available in seconds or :obj:`None` if there is no limit. + + """ + + def __init__(self, timeout: Optional[float]) -> None: + self.deadline: Optional[float] + if timeout is None: + self.deadline = None + else: + self.deadline = time.monotonic() + timeout + + def timeout(self, *, raise_if_elapsed: bool = True) -> Optional[float]: + """ + Calculate a timeout from a deadline. + + Args: + raise_if_elapsed (bool): Whether to raise :exc:`TimeoutError` + if the deadline lapsed. + + Raises: + TimeoutError: If the deadline lapsed. + + Returns: + Time left in seconds or :obj:`None` if there is no limit. + + """ + if self.deadline is None: + return None + timeout = self.deadline - time.monotonic() + if raise_if_elapsed and timeout <= 0: + raise TimeoutError("timed out") + return timeout diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/typing.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/typing.py index e672ba0069e..cc3e3ec0d96 100644 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets/typing.py +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/typing.py @@ -1,5 +1,6 @@ from __future__ import annotations +import http import logging from typing import List, NewType, Optional, Tuple, Union @@ -7,6 +8,7 @@ from typing import List, NewType, Optional, Tuple, Union __all__ = [ "Data", "LoggerLike", + "StatusLike", "Origin", "Subprotocol", "ExtensionName", @@ -30,6 +32,11 @@ LoggerLike = Union[logging.Logger, logging.LoggerAdapter] """Types accepted where a :class:`~logging.Logger` is expected.""" +StatusLike = Union[http.HTTPStatus, int] +""" +Types accepted where an :class:`~http.HTTPStatus` is expected.""" + + Origin = NewType("Origin", str) """Value of a ``Origin`` header.""" diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/uri.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/uri.py index fff0c380645..385090f66ae 100644 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets/uri.py +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/uri.py @@ -33,8 +33,8 @@ class WebSocketURI: port: int path: str query: str - username: Optional[str] - password: Optional[str] + username: Optional[str] = None + password: Optional[str] = None @property def resource_name(self) -> str: diff --git a/tests/wpt/tests/tools/third_party/websockets/src/websockets/version.py b/tests/wpt/tests/tools/third_party/websockets/src/websockets/version.py index c30bfd68f31..d1c99458e2c 100644 --- a/tests/wpt/tests/tools/third_party/websockets/src/websockets/version.py +++ b/tests/wpt/tests/tools/third_party/websockets/src/websockets/version.py @@ -1,5 +1,7 @@ from __future__ import annotations +import importlib.metadata + __all__ = ["tag", "version", "commit"] @@ -18,7 +20,7 @@ __all__ = ["tag", "version", "commit"] released = True -tag = version = commit = "10.3" +tag = version = commit = "12.0" if not released: # pragma: no cover @@ -44,7 +46,11 @@ if not released: # pragma: no cover text=True, ).stdout.strip() # subprocess.run raises FileNotFoundError if git isn't on $PATH. - except (FileNotFoundError, subprocess.CalledProcessError): + except ( + FileNotFoundError, + subprocess.CalledProcessError, + subprocess.TimeoutExpired, + ): pass else: description_re = r"[0-9.]+-([0-9]+)-(g[0-9a-f]{7,}(?:-dirty)?)" @@ -56,8 +62,6 @@ if not released: # pragma: no cover # Read version from package metadata if it is installed. try: - import importlib.metadata # move up when dropping Python 3.7 - return importlib.metadata.version("websockets") except ImportError: pass diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/__init__.py b/tests/wpt/tests/tools/third_party/websockets/tests/__init__.py new file mode 100644 index 00000000000..dd78609f5ba --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/__init__.py @@ -0,0 +1,5 @@ +import logging + + +# Avoid displaying stack traces at the ERROR logging level. +logging.basicConfig(level=logging.CRITICAL) diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/extensions/__init__.py b/tests/wpt/tests/tools/third_party/websockets/tests/extensions/__init__.py new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/extensions/__init__.py diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/extensions/test_base.py b/tests/wpt/tests/tools/third_party/websockets/tests/extensions/test_base.py new file mode 100644 index 00000000000..b18ffb6fb86 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/extensions/test_base.py @@ -0,0 +1,4 @@ +from websockets.extensions.base import * + + +# Abstract classes don't provide any behavior to test. diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/extensions/test_permessage_deflate.py b/tests/wpt/tests/tools/third_party/websockets/tests/extensions/test_permessage_deflate.py new file mode 100644 index 00000000000..0e698566fb9 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/extensions/test_permessage_deflate.py @@ -0,0 +1,977 @@ +import dataclasses +import unittest + +from websockets.exceptions import ( + DuplicateParameter, + InvalidParameterName, + InvalidParameterValue, + NegotiationError, + PayloadTooBig, + ProtocolError, +) +from websockets.extensions.permessage_deflate import * +from websockets.frames import ( + OP_BINARY, + OP_CLOSE, + OP_CONT, + OP_PING, + OP_PONG, + OP_TEXT, + Close, + CloseCode, + Frame, +) + +from .utils import ClientNoOpExtensionFactory, ServerNoOpExtensionFactory + + +class PerMessageDeflateTestsMixin: + def assertExtensionEqual(self, extension1, extension2): + self.assertEqual( + extension1.remote_no_context_takeover, + extension2.remote_no_context_takeover, + ) + self.assertEqual( + extension1.local_no_context_takeover, + extension2.local_no_context_takeover, + ) + self.assertEqual( + extension1.remote_max_window_bits, + extension2.remote_max_window_bits, + ) + self.assertEqual( + extension1.local_max_window_bits, + extension2.local_max_window_bits, + ) + + +class PerMessageDeflateTests(unittest.TestCase, PerMessageDeflateTestsMixin): + def setUp(self): + # Set up an instance of the permessage-deflate extension with the most + # common settings. Since the extension is symmetrical, this instance + # may be used for testing both encoding and decoding. + self.extension = PerMessageDeflate(False, False, 15, 15) + + def test_name(self): + assert self.extension.name == "permessage-deflate" + + def test_repr(self): + self.assertExtensionEqual(eval(repr(self.extension)), self.extension) + + # Control frames aren't encoded or decoded. + + def test_no_encode_decode_ping_frame(self): + frame = Frame(OP_PING, b"") + + self.assertEqual(self.extension.encode(frame), frame) + + self.assertEqual(self.extension.decode(frame), frame) + + def test_no_encode_decode_pong_frame(self): + frame = Frame(OP_PONG, b"") + + self.assertEqual(self.extension.encode(frame), frame) + + self.assertEqual(self.extension.decode(frame), frame) + + def test_no_encode_decode_close_frame(self): + frame = Frame(OP_CLOSE, Close(CloseCode.NORMAL_CLOSURE, "").serialize()) + + self.assertEqual(self.extension.encode(frame), frame) + + self.assertEqual(self.extension.decode(frame), frame) + + # Data frames are encoded and decoded. + + def test_encode_decode_text_frame(self): + frame = Frame(OP_TEXT, "café".encode("utf-8")) + + enc_frame = self.extension.encode(frame) + + self.assertEqual( + enc_frame, + dataclasses.replace(frame, rsv1=True, data=b"JNL;\xbc\x12\x00"), + ) + + dec_frame = self.extension.decode(enc_frame) + + self.assertEqual(dec_frame, frame) + + def test_encode_decode_binary_frame(self): + frame = Frame(OP_BINARY, b"tea") + + enc_frame = self.extension.encode(frame) + + self.assertEqual( + enc_frame, + dataclasses.replace(frame, rsv1=True, data=b"*IM\x04\x00"), + ) + + dec_frame = self.extension.decode(enc_frame) + + self.assertEqual(dec_frame, frame) + + def test_encode_decode_fragmented_text_frame(self): + frame1 = Frame(OP_TEXT, "café".encode("utf-8"), fin=False) + frame2 = Frame(OP_CONT, " & ".encode("utf-8"), fin=False) + frame3 = Frame(OP_CONT, "croissants".encode("utf-8")) + + enc_frame1 = self.extension.encode(frame1) + enc_frame2 = self.extension.encode(frame2) + enc_frame3 = self.extension.encode(frame3) + + self.assertEqual( + enc_frame1, + dataclasses.replace( + frame1, rsv1=True, data=b"JNL;\xbc\x12\x00\x00\x00\xff\xff" + ), + ) + self.assertEqual( + enc_frame2, + dataclasses.replace(frame2, data=b"RPS\x00\x00\x00\x00\xff\xff"), + ) + self.assertEqual( + enc_frame3, + dataclasses.replace(frame3, data=b"J.\xca\xcf,.N\xcc+)\x06\x00"), + ) + + dec_frame1 = self.extension.decode(enc_frame1) + dec_frame2 = self.extension.decode(enc_frame2) + dec_frame3 = self.extension.decode(enc_frame3) + + self.assertEqual(dec_frame1, frame1) + self.assertEqual(dec_frame2, frame2) + self.assertEqual(dec_frame3, frame3) + + def test_encode_decode_fragmented_binary_frame(self): + frame1 = Frame(OP_TEXT, b"tea ", fin=False) + frame2 = Frame(OP_CONT, b"time") + + enc_frame1 = self.extension.encode(frame1) + enc_frame2 = self.extension.encode(frame2) + + self.assertEqual( + enc_frame1, + dataclasses.replace( + frame1, rsv1=True, data=b"*IMT\x00\x00\x00\x00\xff\xff" + ), + ) + self.assertEqual( + enc_frame2, + dataclasses.replace(frame2, data=b"*\xc9\xccM\x05\x00"), + ) + + dec_frame1 = self.extension.decode(enc_frame1) + dec_frame2 = self.extension.decode(enc_frame2) + + self.assertEqual(dec_frame1, frame1) + self.assertEqual(dec_frame2, frame2) + + def test_no_decode_text_frame(self): + frame = Frame(OP_TEXT, "café".encode("utf-8")) + + # Try decoding a frame that wasn't encoded. + self.assertEqual(self.extension.decode(frame), frame) + + def test_no_decode_binary_frame(self): + frame = Frame(OP_TEXT, b"tea") + + # Try decoding a frame that wasn't encoded. + self.assertEqual(self.extension.decode(frame), frame) + + def test_no_decode_fragmented_text_frame(self): + frame1 = Frame(OP_TEXT, "café".encode("utf-8"), fin=False) + frame2 = Frame(OP_CONT, " & ".encode("utf-8"), fin=False) + frame3 = Frame(OP_CONT, "croissants".encode("utf-8")) + + dec_frame1 = self.extension.decode(frame1) + dec_frame2 = self.extension.decode(frame2) + dec_frame3 = self.extension.decode(frame3) + + self.assertEqual(dec_frame1, frame1) + self.assertEqual(dec_frame2, frame2) + self.assertEqual(dec_frame3, frame3) + + def test_no_decode_fragmented_binary_frame(self): + frame1 = Frame(OP_TEXT, b"tea ", fin=False) + frame2 = Frame(OP_CONT, b"time") + + dec_frame1 = self.extension.decode(frame1) + dec_frame2 = self.extension.decode(frame2) + + self.assertEqual(dec_frame1, frame1) + self.assertEqual(dec_frame2, frame2) + + def test_context_takeover(self): + frame = Frame(OP_TEXT, "café".encode("utf-8")) + + enc_frame1 = self.extension.encode(frame) + enc_frame2 = self.extension.encode(frame) + + self.assertEqual(enc_frame1.data, b"JNL;\xbc\x12\x00") + self.assertEqual(enc_frame2.data, b"J\x06\x11\x00\x00") + + def test_remote_no_context_takeover(self): + # No context takeover when decoding messages. + self.extension = PerMessageDeflate(True, False, 15, 15) + + frame = Frame(OP_TEXT, "café".encode("utf-8")) + + enc_frame1 = self.extension.encode(frame) + enc_frame2 = self.extension.encode(frame) + + self.assertEqual(enc_frame1.data, b"JNL;\xbc\x12\x00") + self.assertEqual(enc_frame2.data, b"J\x06\x11\x00\x00") + + dec_frame1 = self.extension.decode(enc_frame1) + self.assertEqual(dec_frame1, frame) + + with self.assertRaises(ProtocolError): + self.extension.decode(enc_frame2) + + def test_local_no_context_takeover(self): + # No context takeover when encoding and decoding messages. + self.extension = PerMessageDeflate(True, True, 15, 15) + + frame = Frame(OP_TEXT, "café".encode("utf-8")) + + enc_frame1 = self.extension.encode(frame) + enc_frame2 = self.extension.encode(frame) + + self.assertEqual(enc_frame1.data, b"JNL;\xbc\x12\x00") + self.assertEqual(enc_frame2.data, b"JNL;\xbc\x12\x00") + + dec_frame1 = self.extension.decode(enc_frame1) + dec_frame2 = self.extension.decode(enc_frame2) + + self.assertEqual(dec_frame1, frame) + self.assertEqual(dec_frame2, frame) + + # Compression settings can be customized. + + def test_compress_settings(self): + # Configure an extension so that no compression actually occurs. + extension = PerMessageDeflate(False, False, 15, 15, {"level": 0}) + + frame = Frame(OP_TEXT, "café".encode("utf-8")) + + enc_frame = extension.encode(frame) + + self.assertEqual( + enc_frame, + dataclasses.replace( + frame, + rsv1=True, + data=b"\x00\x05\x00\xfa\xffcaf\xc3\xa9\x00", # not compressed + ), + ) + + # Frames aren't decoded beyond max_size. + + def test_decompress_max_size(self): + frame = Frame(OP_TEXT, ("a" * 20).encode("utf-8")) + + enc_frame = self.extension.encode(frame) + + self.assertEqual(enc_frame.data, b"JL\xc4\x04\x00\x00") + + with self.assertRaises(PayloadTooBig): + self.extension.decode(enc_frame, max_size=10) + + +class ClientPerMessageDeflateFactoryTests( + unittest.TestCase, PerMessageDeflateTestsMixin +): + def test_name(self): + assert ClientPerMessageDeflateFactory.name == "permessage-deflate" + + def test_init(self): + for config in [ + (False, False, 8, None), # server_max_window_bits ≥ 8 + (False, True, 15, None), # server_max_window_bits ≤ 15 + (True, False, None, 8), # client_max_window_bits ≥ 8 + (True, True, None, 15), # client_max_window_bits ≤ 15 + (False, False, None, True), # client_max_window_bits + (False, False, None, None, {"memLevel": 4}), + ]: + with self.subTest(config=config): + # This does not raise an exception. + ClientPerMessageDeflateFactory(*config) + + def test_init_error(self): + for config in [ + (False, False, 7, 8), # server_max_window_bits < 8 + (False, True, 8, 7), # client_max_window_bits < 8 + (True, False, 16, 15), # server_max_window_bits > 15 + (True, True, 15, 16), # client_max_window_bits > 15 + (False, False, True, None), # server_max_window_bits + (False, False, None, None, {"wbits": 11}), + ]: + with self.subTest(config=config): + with self.assertRaises(ValueError): + ClientPerMessageDeflateFactory(*config) + + def test_get_request_params(self): + for config, result in [ + # Test without any parameter + ( + (False, False, None, None), + [], + ), + # Test server_no_context_takeover + ( + (True, False, None, None), + [("server_no_context_takeover", None)], + ), + # Test client_no_context_takeover + ( + (False, True, None, None), + [("client_no_context_takeover", None)], + ), + # Test server_max_window_bits + ( + (False, False, 10, None), + [("server_max_window_bits", "10")], + ), + # Test client_max_window_bits + ( + (False, False, None, 10), + [("client_max_window_bits", "10")], + ), + ( + (False, False, None, True), + [("client_max_window_bits", None)], + ), + # Test all parameters together + ( + (True, True, 12, 12), + [ + ("server_no_context_takeover", None), + ("client_no_context_takeover", None), + ("server_max_window_bits", "12"), + ("client_max_window_bits", "12"), + ], + ), + ]: + with self.subTest(config=config): + factory = ClientPerMessageDeflateFactory(*config) + self.assertEqual(factory.get_request_params(), result) + + def test_process_response_params(self): + for config, response_params, result in [ + # Test without any parameter + ( + (False, False, None, None), + [], + (False, False, 15, 15), + ), + ( + (False, False, None, None), + [("unknown", None)], + InvalidParameterName, + ), + # Test server_no_context_takeover + ( + (False, False, None, None), + [("server_no_context_takeover", None)], + (True, False, 15, 15), + ), + ( + (True, False, None, None), + [], + NegotiationError, + ), + ( + (True, False, None, None), + [("server_no_context_takeover", None)], + (True, False, 15, 15), + ), + ( + (True, False, None, None), + [("server_no_context_takeover", None)] * 2, + DuplicateParameter, + ), + ( + (True, False, None, None), + [("server_no_context_takeover", "42")], + InvalidParameterValue, + ), + # Test client_no_context_takeover + ( + (False, False, None, None), + [("client_no_context_takeover", None)], + (False, True, 15, 15), + ), + ( + (False, True, None, None), + [], + (False, True, 15, 15), + ), + ( + (False, True, None, None), + [("client_no_context_takeover", None)], + (False, True, 15, 15), + ), + ( + (False, True, None, None), + [("client_no_context_takeover", None)] * 2, + DuplicateParameter, + ), + ( + (False, True, None, None), + [("client_no_context_takeover", "42")], + InvalidParameterValue, + ), + # Test server_max_window_bits + ( + (False, False, None, None), + [("server_max_window_bits", "7")], + NegotiationError, + ), + ( + (False, False, None, None), + [("server_max_window_bits", "10")], + (False, False, 10, 15), + ), + ( + (False, False, None, None), + [("server_max_window_bits", "16")], + NegotiationError, + ), + ( + (False, False, 12, None), + [], + NegotiationError, + ), + ( + (False, False, 12, None), + [("server_max_window_bits", "10")], + (False, False, 10, 15), + ), + ( + (False, False, 12, None), + [("server_max_window_bits", "12")], + (False, False, 12, 15), + ), + ( + (False, False, 12, None), + [("server_max_window_bits", "13")], + NegotiationError, + ), + ( + (False, False, 12, None), + [("server_max_window_bits", "12")] * 2, + DuplicateParameter, + ), + ( + (False, False, 12, None), + [("server_max_window_bits", "42")], + InvalidParameterValue, + ), + # Test client_max_window_bits + ( + (False, False, None, None), + [("client_max_window_bits", "10")], + NegotiationError, + ), + ( + (False, False, None, True), + [], + (False, False, 15, 15), + ), + ( + (False, False, None, True), + [("client_max_window_bits", "7")], + NegotiationError, + ), + ( + (False, False, None, True), + [("client_max_window_bits", "10")], + (False, False, 15, 10), + ), + ( + (False, False, None, True), + [("client_max_window_bits", "16")], + NegotiationError, + ), + ( + (False, False, None, 12), + [], + (False, False, 15, 12), + ), + ( + (False, False, None, 12), + [("client_max_window_bits", "10")], + (False, False, 15, 10), + ), + ( + (False, False, None, 12), + [("client_max_window_bits", "12")], + (False, False, 15, 12), + ), + ( + (False, False, None, 12), + [("client_max_window_bits", "13")], + NegotiationError, + ), + ( + (False, False, None, 12), + [("client_max_window_bits", "12")] * 2, + DuplicateParameter, + ), + ( + (False, False, None, 12), + [("client_max_window_bits", "42")], + InvalidParameterValue, + ), + # Test all parameters together + ( + (True, True, 12, 12), + [ + ("server_no_context_takeover", None), + ("client_no_context_takeover", None), + ("server_max_window_bits", "10"), + ("client_max_window_bits", "10"), + ], + (True, True, 10, 10), + ), + ( + (False, False, None, True), + [ + ("server_no_context_takeover", None), + ("client_no_context_takeover", None), + ("server_max_window_bits", "10"), + ("client_max_window_bits", "10"), + ], + (True, True, 10, 10), + ), + ( + (True, True, 12, 12), + [ + ("server_no_context_takeover", None), + ("server_max_window_bits", "12"), + ], + (True, True, 12, 12), + ), + ]: + with self.subTest(config=config, response_params=response_params): + factory = ClientPerMessageDeflateFactory(*config) + if isinstance(result, type) and issubclass(result, Exception): + with self.assertRaises(result): + factory.process_response_params(response_params, []) + else: + extension = factory.process_response_params(response_params, []) + expected = PerMessageDeflate(*result) + self.assertExtensionEqual(extension, expected) + + def test_process_response_params_deduplication(self): + factory = ClientPerMessageDeflateFactory(False, False, None, None) + with self.assertRaises(NegotiationError): + factory.process_response_params( + [], [PerMessageDeflate(False, False, 15, 15)] + ) + + def test_enable_client_permessage_deflate(self): + for extensions, ( + expected_len, + expected_position, + expected_compress_settings, + ) in [ + ( + None, + (1, 0, {"memLevel": 5}), + ), + ( + [], + (1, 0, {"memLevel": 5}), + ), + ( + [ClientNoOpExtensionFactory()], + (2, 1, {"memLevel": 5}), + ), + ( + [ClientPerMessageDeflateFactory(compress_settings={"memLevel": 7})], + (1, 0, {"memLevel": 7}), + ), + ( + [ + ClientPerMessageDeflateFactory(compress_settings={"memLevel": 7}), + ClientNoOpExtensionFactory(), + ], + (2, 0, {"memLevel": 7}), + ), + ( + [ + ClientNoOpExtensionFactory(), + ClientPerMessageDeflateFactory(compress_settings={"memLevel": 7}), + ], + (2, 1, {"memLevel": 7}), + ), + ]: + with self.subTest(extensions=extensions): + extensions = enable_client_permessage_deflate(extensions) + self.assertEqual(len(extensions), expected_len) + extension = extensions[expected_position] + self.assertIsInstance(extension, ClientPerMessageDeflateFactory) + self.assertEqual( + extension.compress_settings, + expected_compress_settings, + ) + + +class ServerPerMessageDeflateFactoryTests( + unittest.TestCase, PerMessageDeflateTestsMixin +): + def test_name(self): + assert ServerPerMessageDeflateFactory.name == "permessage-deflate" + + def test_init(self): + for config in [ + (False, False, 8, None), # server_max_window_bits ≥ 8 + (False, True, 15, None), # server_max_window_bits ≤ 15 + (True, False, None, 8), # client_max_window_bits ≥ 8 + (True, True, None, 15), # client_max_window_bits ≤ 15 + (False, False, None, None, {"memLevel": 4}), + (False, False, None, 12, {}, True), # require_client_max_window_bits + ]: + with self.subTest(config=config): + # This does not raise an exception. + ServerPerMessageDeflateFactory(*config) + + def test_init_error(self): + for config in [ + (False, False, 7, 8), # server_max_window_bits < 8 + (False, True, 8, 7), # client_max_window_bits < 8 + (True, False, 16, 15), # server_max_window_bits > 15 + (True, True, 15, 16), # client_max_window_bits > 15 + (False, False, None, True), # client_max_window_bits + (False, False, True, None), # server_max_window_bits + (False, False, None, None, {"wbits": 11}), + (False, False, None, None, {}, True), # require_client_max_window_bits + ]: + with self.subTest(config=config): + with self.assertRaises(ValueError): + ServerPerMessageDeflateFactory(*config) + + def test_process_request_params(self): + # Parameters in result appear swapped vs. config because the order is + # (remote, local) vs. (server, client). + for config, request_params, response_params, result in [ + # Test without any parameter + ( + (False, False, None, None), + [], + [], + (False, False, 15, 15), + ), + ( + (False, False, None, None), + [("unknown", None)], + None, + InvalidParameterName, + ), + # Test server_no_context_takeover + ( + (False, False, None, None), + [("server_no_context_takeover", None)], + [("server_no_context_takeover", None)], + (False, True, 15, 15), + ), + ( + (True, False, None, None), + [], + [("server_no_context_takeover", None)], + (False, True, 15, 15), + ), + ( + (True, False, None, None), + [("server_no_context_takeover", None)], + [("server_no_context_takeover", None)], + (False, True, 15, 15), + ), + ( + (True, False, None, None), + [("server_no_context_takeover", None)] * 2, + None, + DuplicateParameter, + ), + ( + (True, False, None, None), + [("server_no_context_takeover", "42")], + None, + InvalidParameterValue, + ), + # Test client_no_context_takeover + ( + (False, False, None, None), + [("client_no_context_takeover", None)], + [("client_no_context_takeover", None)], # doesn't matter + (True, False, 15, 15), + ), + ( + (False, True, None, None), + [], + [("client_no_context_takeover", None)], + (True, False, 15, 15), + ), + ( + (False, True, None, None), + [("client_no_context_takeover", None)], + [("client_no_context_takeover", None)], # doesn't matter + (True, False, 15, 15), + ), + ( + (False, True, None, None), + [("client_no_context_takeover", None)] * 2, + None, + DuplicateParameter, + ), + ( + (False, True, None, None), + [("client_no_context_takeover", "42")], + None, + InvalidParameterValue, + ), + # Test server_max_window_bits + ( + (False, False, None, None), + [("server_max_window_bits", "7")], + None, + NegotiationError, + ), + ( + (False, False, None, None), + [("server_max_window_bits", "10")], + [("server_max_window_bits", "10")], + (False, False, 15, 10), + ), + ( + (False, False, None, None), + [("server_max_window_bits", "16")], + None, + NegotiationError, + ), + ( + (False, False, 12, None), + [], + [("server_max_window_bits", "12")], + (False, False, 15, 12), + ), + ( + (False, False, 12, None), + [("server_max_window_bits", "10")], + [("server_max_window_bits", "10")], + (False, False, 15, 10), + ), + ( + (False, False, 12, None), + [("server_max_window_bits", "12")], + [("server_max_window_bits", "12")], + (False, False, 15, 12), + ), + ( + (False, False, 12, None), + [("server_max_window_bits", "13")], + [("server_max_window_bits", "12")], + (False, False, 15, 12), + ), + ( + (False, False, 12, None), + [("server_max_window_bits", "12")] * 2, + None, + DuplicateParameter, + ), + ( + (False, False, 12, None), + [("server_max_window_bits", "42")], + None, + InvalidParameterValue, + ), + # Test client_max_window_bits + ( + (False, False, None, None), + [("client_max_window_bits", None)], + [], + (False, False, 15, 15), + ), + ( + (False, False, None, None), + [("client_max_window_bits", "7")], + None, + InvalidParameterValue, + ), + ( + (False, False, None, None), + [("client_max_window_bits", "10")], + [("client_max_window_bits", "10")], # doesn't matter + (False, False, 10, 15), + ), + ( + (False, False, None, None), + [("client_max_window_bits", "16")], + None, + InvalidParameterValue, + ), + ( + (False, False, None, 12), + [], + [], + (False, False, 15, 15), + ), + ( + (False, False, None, 12, {}, True), + [], + None, + NegotiationError, + ), + ( + (False, False, None, 12), + [("client_max_window_bits", None)], + [("client_max_window_bits", "12")], + (False, False, 12, 15), + ), + ( + (False, False, None, 12), + [("client_max_window_bits", "10")], + [("client_max_window_bits", "10")], + (False, False, 10, 15), + ), + ( + (False, False, None, 12), + [("client_max_window_bits", "12")], + [("client_max_window_bits", "12")], # doesn't matter + (False, False, 12, 15), + ), + ( + (False, False, None, 12), + [("client_max_window_bits", "13")], + [("client_max_window_bits", "12")], # doesn't matter + (False, False, 12, 15), + ), + ( + (False, False, None, 12), + [("client_max_window_bits", "12")] * 2, + None, + DuplicateParameter, + ), + ( + (False, False, None, 12), + [("client_max_window_bits", "42")], + None, + InvalidParameterValue, + ), + # Test all parameters together + ( + (True, True, 12, 12), + [ + ("server_no_context_takeover", None), + ("client_no_context_takeover", None), + ("server_max_window_bits", "10"), + ("client_max_window_bits", "10"), + ], + [ + ("server_no_context_takeover", None), + ("client_no_context_takeover", None), + ("server_max_window_bits", "10"), + ("client_max_window_bits", "10"), + ], + (True, True, 10, 10), + ), + ( + (False, False, None, None), + [ + ("server_no_context_takeover", None), + ("client_no_context_takeover", None), + ("server_max_window_bits", "10"), + ("client_max_window_bits", "10"), + ], + [ + ("server_no_context_takeover", None), + ("client_no_context_takeover", None), + ("server_max_window_bits", "10"), + ("client_max_window_bits", "10"), + ], + (True, True, 10, 10), + ), + ( + (True, True, 12, 12), + [("client_max_window_bits", None)], + [ + ("server_no_context_takeover", None), + ("client_no_context_takeover", None), + ("server_max_window_bits", "12"), + ("client_max_window_bits", "12"), + ], + (True, True, 12, 12), + ), + ]: + with self.subTest( + config=config, + request_params=request_params, + response_params=response_params, + ): + factory = ServerPerMessageDeflateFactory(*config) + if isinstance(result, type) and issubclass(result, Exception): + with self.assertRaises(result): + factory.process_request_params(request_params, []) + else: + params, extension = factory.process_request_params( + request_params, [] + ) + self.assertEqual(params, response_params) + expected = PerMessageDeflate(*result) + self.assertExtensionEqual(extension, expected) + + def test_process_response_params_deduplication(self): + factory = ServerPerMessageDeflateFactory(False, False, None, None) + with self.assertRaises(NegotiationError): + factory.process_request_params( + [], [PerMessageDeflate(False, False, 15, 15)] + ) + + def test_enable_server_permessage_deflate(self): + for extensions, ( + expected_len, + expected_position, + expected_compress_settings, + ) in [ + ( + None, + (1, 0, {"memLevel": 5}), + ), + ( + [], + (1, 0, {"memLevel": 5}), + ), + ( + [ServerNoOpExtensionFactory()], + (2, 1, {"memLevel": 5}), + ), + ( + [ServerPerMessageDeflateFactory(compress_settings={"memLevel": 7})], + (1, 0, {"memLevel": 7}), + ), + ( + [ + ServerPerMessageDeflateFactory(compress_settings={"memLevel": 7}), + ServerNoOpExtensionFactory(), + ], + (2, 0, {"memLevel": 7}), + ), + ( + [ + ServerNoOpExtensionFactory(), + ServerPerMessageDeflateFactory(compress_settings={"memLevel": 7}), + ], + (2, 1, {"memLevel": 7}), + ), + ]: + with self.subTest(extensions=extensions): + extensions = enable_server_permessage_deflate(extensions) + self.assertEqual(len(extensions), expected_len) + extension = extensions[expected_position] + self.assertIsInstance(extension, ServerPerMessageDeflateFactory) + self.assertEqual( + extension.compress_settings, + expected_compress_settings, + ) diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/extensions/utils.py b/tests/wpt/tests/tools/third_party/websockets/tests/extensions/utils.py new file mode 100644 index 00000000000..24fb74b4e6e --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/extensions/utils.py @@ -0,0 +1,113 @@ +import dataclasses + +from websockets.exceptions import NegotiationError + + +class OpExtension: + name = "x-op" + + def __init__(self, op=None): + self.op = op + + def decode(self, frame, *, max_size=None): + return frame # pragma: no cover + + def encode(self, frame): + return frame # pragma: no cover + + def __eq__(self, other): + return isinstance(other, OpExtension) and self.op == other.op + + +class ClientOpExtensionFactory: + name = "x-op" + + def __init__(self, op=None): + self.op = op + + def get_request_params(self): + return [("op", self.op)] + + def process_response_params(self, params, accepted_extensions): + if params != [("op", self.op)]: + raise NegotiationError() + return OpExtension(self.op) + + +class ServerOpExtensionFactory: + name = "x-op" + + def __init__(self, op=None): + self.op = op + + def process_request_params(self, params, accepted_extensions): + if params != [("op", self.op)]: + raise NegotiationError() + return [("op", self.op)], OpExtension(self.op) + + +class NoOpExtension: + name = "x-no-op" + + def __repr__(self): + return "NoOpExtension()" + + def decode(self, frame, *, max_size=None): + return frame + + def encode(self, frame): + return frame + + +class ClientNoOpExtensionFactory: + name = "x-no-op" + + def get_request_params(self): + return [] + + def process_response_params(self, params, accepted_extensions): + if params: + raise NegotiationError() + return NoOpExtension() + + +class ServerNoOpExtensionFactory: + name = "x-no-op" + + def __init__(self, params=None): + self.params = params or [] + + def process_request_params(self, params, accepted_extensions): + return self.params, NoOpExtension() + + +class Rsv2Extension: + name = "x-rsv2" + + def decode(self, frame, *, max_size=None): + assert frame.rsv2 + return dataclasses.replace(frame, rsv2=False) + + def encode(self, frame): + assert not frame.rsv2 + return dataclasses.replace(frame, rsv2=True) + + def __eq__(self, other): + return isinstance(other, Rsv2Extension) + + +class ClientRsv2ExtensionFactory: + name = "x-rsv2" + + def get_request_params(self): + return [] + + def process_response_params(self, params, accepted_extensions): + return Rsv2Extension() + + +class ServerRsv2ExtensionFactory: + name = "x-rsv2" + + def process_request_params(self, params, accepted_extensions): + return [], Rsv2Extension() diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/legacy/__init__.py b/tests/wpt/tests/tools/third_party/websockets/tests/legacy/__init__.py new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/legacy/__init__.py diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/legacy/test_auth.py b/tests/wpt/tests/tools/third_party/websockets/tests/legacy/test_auth.py new file mode 100644 index 00000000000..3754bcf3a5d --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/legacy/test_auth.py @@ -0,0 +1,184 @@ +import hmac +import unittest +import urllib.error + +from websockets.exceptions import InvalidStatusCode +from websockets.headers import build_authorization_basic +from websockets.legacy.auth import * +from websockets.legacy.auth import is_credentials + +from .test_client_server import ClientServerTestsMixin, with_client, with_server +from .utils import AsyncioTestCase + + +class AuthTests(unittest.TestCase): + def test_is_credentials(self): + self.assertTrue(is_credentials(("username", "password"))) + + def test_is_not_credentials(self): + self.assertFalse(is_credentials(None)) + self.assertFalse(is_credentials("username")) + + +class CustomWebSocketServerProtocol(BasicAuthWebSocketServerProtocol): + async def process_request(self, path, request_headers): + type(self).used = True + return await super().process_request(path, request_headers) + + +class CheckWebSocketServerProtocol(BasicAuthWebSocketServerProtocol): + async def check_credentials(self, username, password): + return hmac.compare_digest(password, "letmein") + + +class AuthClientServerTests(ClientServerTestsMixin, AsyncioTestCase): + create_protocol = basic_auth_protocol_factory( + realm="auth-tests", credentials=("hello", "iloveyou") + ) + + @with_server(create_protocol=create_protocol) + @with_client(user_info=("hello", "iloveyou")) + def test_basic_auth(self): + req_headers = self.client.request_headers + resp_headers = self.client.response_headers + self.assertEqual(req_headers["Authorization"], "Basic aGVsbG86aWxvdmV5b3U=") + self.assertNotIn("WWW-Authenticate", resp_headers) + + self.loop.run_until_complete(self.client.send("Hello!")) + self.loop.run_until_complete(self.client.recv()) + + def test_basic_auth_server_no_credentials(self): + with self.assertRaises(TypeError) as raised: + basic_auth_protocol_factory(realm="auth-tests", credentials=None) + self.assertEqual( + str(raised.exception), "provide either credentials or check_credentials" + ) + + def test_basic_auth_server_bad_credentials(self): + with self.assertRaises(TypeError) as raised: + basic_auth_protocol_factory(realm="auth-tests", credentials=42) + self.assertEqual(str(raised.exception), "invalid credentials argument: 42") + + create_protocol_multiple_credentials = basic_auth_protocol_factory( + realm="auth-tests", + credentials=[("hello", "iloveyou"), ("goodbye", "stillloveu")], + ) + + @with_server(create_protocol=create_protocol_multiple_credentials) + @with_client(user_info=("hello", "iloveyou")) + def test_basic_auth_server_multiple_credentials(self): + self.loop.run_until_complete(self.client.send("Hello!")) + self.loop.run_until_complete(self.client.recv()) + + def test_basic_auth_bad_multiple_credentials(self): + with self.assertRaises(TypeError) as raised: + basic_auth_protocol_factory( + realm="auth-tests", credentials=[("hello", "iloveyou"), 42] + ) + self.assertEqual( + str(raised.exception), + "invalid credentials argument: [('hello', 'iloveyou'), 42]", + ) + + async def check_credentials(username, password): + return hmac.compare_digest(password, "iloveyou") + + create_protocol_check_credentials = basic_auth_protocol_factory( + realm="auth-tests", + check_credentials=check_credentials, + ) + + @with_server(create_protocol=create_protocol_check_credentials) + @with_client(user_info=("hello", "iloveyou")) + def test_basic_auth_check_credentials(self): + self.loop.run_until_complete(self.client.send("Hello!")) + self.loop.run_until_complete(self.client.recv()) + + create_protocol_custom_protocol = basic_auth_protocol_factory( + realm="auth-tests", + credentials=[("hello", "iloveyou")], + create_protocol=CustomWebSocketServerProtocol, + ) + + @with_server(create_protocol=create_protocol_custom_protocol) + @with_client(user_info=("hello", "iloveyou")) + def test_basic_auth_custom_protocol(self): + self.assertTrue(CustomWebSocketServerProtocol.used) + del CustomWebSocketServerProtocol.used + self.loop.run_until_complete(self.client.send("Hello!")) + self.loop.run_until_complete(self.client.recv()) + + @with_server(create_protocol=CheckWebSocketServerProtocol) + @with_client(user_info=("hello", "letmein")) + def test_basic_auth_custom_protocol_subclass(self): + self.loop.run_until_complete(self.client.send("Hello!")) + self.loop.run_until_complete(self.client.recv()) + + # CustomWebSocketServerProtocol doesn't override check_credentials + @with_server(create_protocol=CustomWebSocketServerProtocol) + def test_basic_auth_defaults_to_deny_all(self): + with self.assertRaises(InvalidStatusCode) as raised: + self.start_client(user_info=("hello", "iloveyou")) + self.assertEqual(raised.exception.status_code, 401) + + @with_server(create_protocol=create_protocol) + def test_basic_auth_missing_credentials(self): + with self.assertRaises(InvalidStatusCode) as raised: + self.start_client() + self.assertEqual(raised.exception.status_code, 401) + + @with_server(create_protocol=create_protocol) + def test_basic_auth_missing_credentials_details(self): + with self.assertRaises(urllib.error.HTTPError) as raised: + self.loop.run_until_complete(self.make_http_request()) + self.assertEqual(raised.exception.code, 401) + self.assertEqual( + raised.exception.headers["WWW-Authenticate"], + 'Basic realm="auth-tests", charset="UTF-8"', + ) + self.assertEqual(raised.exception.read().decode(), "Missing credentials\n") + + @with_server(create_protocol=create_protocol) + def test_basic_auth_unsupported_credentials(self): + with self.assertRaises(InvalidStatusCode) as raised: + self.start_client(extra_headers={"Authorization": "Digest ..."}) + self.assertEqual(raised.exception.status_code, 401) + + @with_server(create_protocol=create_protocol) + def test_basic_auth_unsupported_credentials_details(self): + with self.assertRaises(urllib.error.HTTPError) as raised: + self.loop.run_until_complete( + self.make_http_request(headers={"Authorization": "Digest ..."}) + ) + self.assertEqual(raised.exception.code, 401) + self.assertEqual( + raised.exception.headers["WWW-Authenticate"], + 'Basic realm="auth-tests", charset="UTF-8"', + ) + self.assertEqual(raised.exception.read().decode(), "Unsupported credentials\n") + + @with_server(create_protocol=create_protocol) + def test_basic_auth_invalid_username(self): + with self.assertRaises(InvalidStatusCode) as raised: + self.start_client(user_info=("goodbye", "iloveyou")) + self.assertEqual(raised.exception.status_code, 401) + + @with_server(create_protocol=create_protocol) + def test_basic_auth_invalid_password(self): + with self.assertRaises(InvalidStatusCode) as raised: + self.start_client(user_info=("hello", "ihateyou")) + self.assertEqual(raised.exception.status_code, 401) + + @with_server(create_protocol=create_protocol) + def test_basic_auth_invalid_credentials_details(self): + with self.assertRaises(urllib.error.HTTPError) as raised: + authorization = build_authorization_basic("hello", "ihateyou") + self.loop.run_until_complete( + self.make_http_request(headers={"Authorization": authorization}) + ) + self.assertEqual(raised.exception.code, 401) + self.assertEqual( + raised.exception.headers["WWW-Authenticate"], + 'Basic realm="auth-tests", charset="UTF-8"', + ) + self.assertEqual(raised.exception.read().decode(), "Invalid credentials\n") diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/legacy/test_client_server.py b/tests/wpt/tests/tools/third_party/websockets/tests/legacy/test_client_server.py new file mode 100644 index 00000000000..c49d91b7075 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/legacy/test_client_server.py @@ -0,0 +1,1636 @@ +import asyncio +import contextlib +import functools +import http +import logging +import platform +import random +import socket +import ssl +import sys +import unittest +import unittest.mock +import urllib.error +import urllib.request +import warnings + +from websockets.datastructures import Headers +from websockets.exceptions import ( + ConnectionClosed, + InvalidHandshake, + InvalidHeader, + InvalidStatusCode, + NegotiationError, +) +from websockets.extensions.permessage_deflate import ( + ClientPerMessageDeflateFactory, + PerMessageDeflate, + ServerPerMessageDeflateFactory, +) +from websockets.frames import CloseCode +from websockets.http import USER_AGENT +from websockets.legacy.client import * +from websockets.legacy.compatibility import asyncio_timeout +from websockets.legacy.handshake import build_response +from websockets.legacy.http import read_response +from websockets.legacy.server import * +from websockets.protocol import State +from websockets.uri import parse_uri + +from ..extensions.utils import ( + ClientNoOpExtensionFactory, + NoOpExtension, + ServerNoOpExtensionFactory, +) +from ..utils import CERTIFICATE, MS, temp_unix_socket_path +from .utils import AsyncioTestCase + + +async def default_handler(ws): + if ws.path == "/deprecated_attributes": + await ws.recv() # delay that allows catching warnings + await ws.send(repr((ws.host, ws.port, ws.secure))) + elif ws.path == "/close_timeout": + await ws.send(repr(ws.close_timeout)) + elif ws.path == "/path": + await ws.send(str(ws.path)) + elif ws.path == "/headers": + await ws.send(repr(ws.request_headers)) + await ws.send(repr(ws.response_headers)) + elif ws.path == "/extensions": + await ws.send(repr(ws.extensions)) + elif ws.path == "/subprotocol": + await ws.send(repr(ws.subprotocol)) + elif ws.path == "/slow_stop": + await ws.wait_closed() + await asyncio.sleep(2 * MS) + else: + await ws.send((await ws.recv())) + + +async def redirect_request(path, headers, test, status): + if path == "/absolute_redirect": + location = get_server_uri(test.server, test.secure, "/") + elif path == "/relative_redirect": + location = "/" + elif path == "/infinite": + location = get_server_uri(test.server, test.secure, "/infinite") + elif path == "/force_insecure": + location = get_server_uri(test.server, False, "/") + elif path == "/missing_location": + return status, {}, b"" + else: + return None + return status, {"Location": location}, b"" + + +@contextlib.contextmanager +def temp_test_server(test, **kwargs): + test.start_server(**kwargs) + try: + yield + finally: + test.stop_server() + + +def temp_test_redirecting_server(test, status=http.HTTPStatus.FOUND, **kwargs): + process_request = functools.partial(redirect_request, test=test, status=status) + return temp_test_server(test, process_request=process_request, **kwargs) + + +@contextlib.contextmanager +def temp_test_client(test, *args, **kwargs): + test.start_client(*args, **kwargs) + try: + yield + finally: + test.stop_client() + + +def with_manager(manager, *args, **kwargs): + """ + Return a decorator that wraps a function with a context manager. + + """ + + def decorate(func): + @functools.wraps(func) + def _decorate(self, *_args, **_kwargs): + with manager(self, *args, **kwargs): + return func(self, *_args, **_kwargs) + + return _decorate + + return decorate + + +def with_server(**kwargs): + """ + Return a decorator for TestCase methods that starts and stops a server. + + """ + return with_manager(temp_test_server, **kwargs) + + +def with_client(*args, **kwargs): + """ + Return a decorator for TestCase methods that starts and stops a client. + + """ + return with_manager(temp_test_client, *args, **kwargs) + + +def get_server_address(server): + """ + Return an address on which the given server listens. + + """ + # Pick a random socket in order to test both IPv4 and IPv6 on systems + # where both are available. Randomizing tests is usually a bad idea. If + # needed, either use the first socket, or test separately IPv4 and IPv6. + server_socket = random.choice(server.sockets) + + if server_socket.family == socket.AF_INET6: # pragma: no cover + return server_socket.getsockname()[:2] # (no IPv6 on CI) + elif server_socket.family == socket.AF_INET: + return server_socket.getsockname() + else: # pragma: no cover + raise ValueError("expected an IPv6, IPv4, or Unix socket") + + +def get_server_uri(server, secure=False, resource_name="/", user_info=None): + """ + Return a WebSocket URI for connecting to the given server. + + """ + proto = "wss" if secure else "ws" + user_info = ":".join(user_info) + "@" if user_info else "" + host, port = get_server_address(server) + if ":" in host: # IPv6 address + host = f"[{host}]" + return f"{proto}://{user_info}{host}:{port}{resource_name}" + + +class UnauthorizedServerProtocol(WebSocketServerProtocol): + async def process_request(self, path, request_headers): + # Test returning headers as a Headers instance (1/3) + return http.HTTPStatus.UNAUTHORIZED, Headers([("X-Access", "denied")]), b"" + + +class ForbiddenServerProtocol(WebSocketServerProtocol): + async def process_request(self, path, request_headers): + # Test returning headers as a dict (2/3) + return http.HTTPStatus.FORBIDDEN, {"X-Access": "denied"}, b"" + + +class HealthCheckServerProtocol(WebSocketServerProtocol): + async def process_request(self, path, request_headers): + # Test returning headers as a list of pairs (3/3) + if path == "/__health__/": + return http.HTTPStatus.OK, [("X-Access", "OK")], b"status = green\n" + + +class ProcessRequestReturningIntProtocol(WebSocketServerProtocol): + async def process_request(self, path, request_headers): + assert path == "/__health__/" + return 200, [], b"OK\n" + + +class SlowOpeningHandshakeProtocol(WebSocketServerProtocol): + async def process_request(self, path, request_headers): + await asyncio.sleep(10 * MS) + + +class FooClientProtocol(WebSocketClientProtocol): + pass + + +class BarClientProtocol(WebSocketClientProtocol): + pass + + +class ClientServerTestsMixin: + secure = False + + def setUp(self): + super().setUp() + self.server = None + + def start_server(self, deprecation_warnings=None, **kwargs): + handler = kwargs.pop("handler", default_handler) + # Disable compression by default in tests. + kwargs.setdefault("compression", None) + # Disable pings by default in tests. + kwargs.setdefault("ping_interval", None) + + # This logic is encapsulated in a coroutine to prevent it from executing + # before the event loop is running which causes asyncio.get_event_loop() + # to raise a DeprecationWarning on Python ≥ 3.10. + async def start_server(): + return await serve(handler, "localhost", 0, **kwargs) + + with warnings.catch_warnings(record=True) as recorded_warnings: + warnings.simplefilter("always") + self.server = self.loop.run_until_complete(start_server()) + + expected_warnings = [] if deprecation_warnings is None else deprecation_warnings + self.assertDeprecationWarnings(recorded_warnings, expected_warnings) + + def start_client( + self, resource_name="/", user_info=None, deprecation_warnings=None, **kwargs + ): + # Disable compression by default in tests. + kwargs.setdefault("compression", None) + # Disable pings by default in tests. + kwargs.setdefault("ping_interval", None) + + secure = kwargs.get("ssl") is not None + try: + server_uri = kwargs.pop("uri") + except KeyError: + server_uri = get_server_uri(self.server, secure, resource_name, user_info) + + # This logic is encapsulated in a coroutine to prevent it from executing + # before the event loop is running which causes asyncio.get_event_loop() + # to raise a DeprecationWarning on Python ≥ 3.10. + async def start_client(): + return await connect(server_uri, **kwargs) + + with warnings.catch_warnings(record=True) as recorded_warnings: + warnings.simplefilter("always") + self.client = self.loop.run_until_complete(start_client()) + + expected_warnings = [] if deprecation_warnings is None else deprecation_warnings + self.assertDeprecationWarnings(recorded_warnings, expected_warnings) + + def stop_client(self): + self.loop.run_until_complete( + asyncio.wait_for(self.client.close_connection_task, timeout=1) + ) + + def stop_server(self): + self.server.close() + self.loop.run_until_complete( + asyncio.wait_for(self.server.wait_closed(), timeout=1) + ) + + @contextlib.contextmanager + def temp_server(self, **kwargs): + with temp_test_server(self, **kwargs): + yield + + @contextlib.contextmanager + def temp_client(self, *args, **kwargs): + with temp_test_client(self, *args, **kwargs): + yield + + def make_http_request(self, path="/", headers=None): + if headers is None: + headers = {} + + # Set url to 'https?://<host>:<port><path>'. + url = get_server_uri( + self.server, resource_name=path, secure=self.secure + ).replace("ws", "http") + + request = urllib.request.Request(url=url, headers=headers) + + if self.secure: + open_health_check = functools.partial( + urllib.request.urlopen, request, context=self.client_context + ) + else: + open_health_check = functools.partial(urllib.request.urlopen, request) + + return self.loop.run_in_executor(None, open_health_check) + + +class SecureClientServerTestsMixin(ClientServerTestsMixin): + secure = True + + @property + def server_context(self): + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ssl_context.load_cert_chain(CERTIFICATE) + return ssl_context + + @property + def client_context(self): + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + ssl_context.load_verify_locations(CERTIFICATE) + return ssl_context + + def start_server(self, **kwargs): + kwargs.setdefault("ssl", self.server_context) + super().start_server(**kwargs) + + def start_client(self, path="/", **kwargs): + kwargs.setdefault("ssl", self.client_context) + super().start_client(path, **kwargs) + + +class CommonClientServerTests: + """ + Mixin that defines most tests but doesn't inherit unittest.TestCase. + + Tests are run by the ClientServerTests and SecureClientServerTests subclasses. + + """ + + @with_server() + @with_client() + def test_basic(self): + self.loop.run_until_complete(self.client.send("Hello!")) + reply = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(reply, "Hello!") + + def test_redirect(self): + redirect_statuses = [ + http.HTTPStatus.MOVED_PERMANENTLY, + http.HTTPStatus.FOUND, + http.HTTPStatus.SEE_OTHER, + http.HTTPStatus.TEMPORARY_REDIRECT, + http.HTTPStatus.PERMANENT_REDIRECT, + ] + for status in redirect_statuses: + with temp_test_redirecting_server(self, status): + with self.temp_client("/absolute_redirect"): + self.loop.run_until_complete(self.client.send("Hello!")) + reply = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(reply, "Hello!") + + def test_redirect_relative_location(self): + with temp_test_redirecting_server(self): + with self.temp_client("/relative_redirect"): + self.loop.run_until_complete(self.client.send("Hello!")) + reply = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(reply, "Hello!") + + def test_infinite_redirect(self): + with temp_test_redirecting_server(self): + with self.assertRaises(InvalidHandshake): + with self.temp_client("/infinite"): + self.fail("did not raise") + + def test_redirect_missing_location(self): + with temp_test_redirecting_server(self): + with self.assertRaises(InvalidHeader): + with self.temp_client("/missing_location"): + self.fail("did not raise") + + def test_loop_backwards_compatibility(self): + with self.temp_server( + loop=self.loop, + deprecation_warnings=["remove loop argument"], + ): + with self.temp_client( + loop=self.loop, + deprecation_warnings=["remove loop argument"], + ): + self.loop.run_until_complete(self.client.send("Hello!")) + reply = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(reply, "Hello!") + + @with_server() + def test_explicit_host_port(self): + uri = get_server_uri(self.server, self.secure) + wsuri = parse_uri(uri) + + # Change host and port to invalid values. + scheme = "wss" if wsuri.secure else "ws" + port = 65535 - wsuri.port + changed_uri = f"{scheme}://example.com:{port}/" + + with self.temp_client(uri=changed_uri, host=wsuri.host, port=wsuri.port): + self.loop.run_until_complete(self.client.send("Hello!")) + reply = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(reply, "Hello!") + + @with_server() + def test_explicit_socket(self): + class TrackedSocket(socket.socket): + def __init__(self, *args, **kwargs): + self.used_for_read = False + self.used_for_write = False + super().__init__(*args, **kwargs) + + def recv(self, *args, **kwargs): + self.used_for_read = True + return super().recv(*args, **kwargs) + + def recv_into(self, *args, **kwargs): + self.used_for_read = True + return super().recv_into(*args, **kwargs) + + def send(self, *args, **kwargs): + self.used_for_write = True + return super().send(*args, **kwargs) + + server_socket = [ + sock for sock in self.server.sockets if sock.family == socket.AF_INET + ][0] + client_socket = TrackedSocket(socket.AF_INET, socket.SOCK_STREAM) + client_socket.connect(server_socket.getsockname()) + + try: + self.assertFalse(client_socket.used_for_read) + self.assertFalse(client_socket.used_for_write) + + with self.temp_client(sock=client_socket): + self.loop.run_until_complete(self.client.send("Hello!")) + reply = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(reply, "Hello!") + + self.assertTrue(client_socket.used_for_read) + self.assertTrue(client_socket.used_for_write) + + finally: + client_socket.close() + + @unittest.skipUnless(hasattr(socket, "AF_UNIX"), "this test requires Unix sockets") + def test_unix_socket(self): + with temp_unix_socket_path() as path: + # Like self.start_server() but with unix_serve(). + async def start_server(): + return await unix_serve(default_handler, path) + + self.server = self.loop.run_until_complete(start_server()) + + try: + # Like self.start_client() but with unix_connect() + async def start_client(): + return await unix_connect(path) + + self.client = self.loop.run_until_complete(start_client()) + + try: + self.loop.run_until_complete(self.client.send("Hello!")) + reply = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(reply, "Hello!") + + finally: + self.stop_client() + + finally: + self.stop_server() + + def test_ws_handler_argument_backwards_compatibility(self): + async def handler_with_path(ws, path): + await ws.send(path) + + with self.temp_server( + handler=handler_with_path, + # Enable deprecation warning and announce deprecation in 11.0. + # deprecation_warnings=["remove second argument of ws_handler"], + ): + with self.temp_client("/path"): + self.assertEqual( + self.loop.run_until_complete(self.client.recv()), + "/path", + ) + + def test_ws_handler_argument_backwards_compatibility_partial(self): + async def handler_with_path(ws, path, extra): + await ws.send(path) + + bound_handler_with_path = functools.partial(handler_with_path, extra=None) + + with self.temp_server( + handler=bound_handler_with_path, + # Enable deprecation warning and announce deprecation in 11.0. + # deprecation_warnings=["remove second argument of ws_handler"], + ): + with self.temp_client("/path"): + self.assertEqual( + self.loop.run_until_complete(self.client.recv()), + "/path", + ) + + async def process_request_OK(path, request_headers): + return http.HTTPStatus.OK, [], b"OK\n" + + @with_server(process_request=process_request_OK) + def test_process_request_argument(self): + response = self.loop.run_until_complete(self.make_http_request("/")) + + with contextlib.closing(response): + self.assertEqual(response.code, 200) + + def legacy_process_request_OK(path, request_headers): + return http.HTTPStatus.OK, [], b"OK\n" + + @with_server(process_request=legacy_process_request_OK) + def test_process_request_argument_backwards_compatibility(self): + with warnings.catch_warnings(record=True) as recorded_warnings: + warnings.simplefilter("always") + response = self.loop.run_until_complete(self.make_http_request("/")) + + with contextlib.closing(response): + self.assertEqual(response.code, 200) + + self.assertDeprecationWarnings( + recorded_warnings, ["declare process_request as a coroutine"] + ) + + class ProcessRequestOKServerProtocol(WebSocketServerProtocol): + async def process_request(self, path, request_headers): + return http.HTTPStatus.OK, [], b"OK\n" + + @with_server(create_protocol=ProcessRequestOKServerProtocol) + def test_process_request_override(self): + response = self.loop.run_until_complete(self.make_http_request("/")) + + with contextlib.closing(response): + self.assertEqual(response.code, 200) + + class LegacyProcessRequestOKServerProtocol(WebSocketServerProtocol): + def process_request(self, path, request_headers): + return http.HTTPStatus.OK, [], b"OK\n" + + @with_server(create_protocol=LegacyProcessRequestOKServerProtocol) + def test_process_request_override_backwards_compatibility(self): + with warnings.catch_warnings(record=True) as recorded_warnings: + warnings.simplefilter("always") + response = self.loop.run_until_complete(self.make_http_request("/")) + + with contextlib.closing(response): + self.assertEqual(response.code, 200) + + self.assertDeprecationWarnings( + recorded_warnings, ["declare process_request as a coroutine"] + ) + + def select_subprotocol_chat(client_subprotocols, server_subprotocols): + return "chat" + + @with_server( + subprotocols=["superchat", "chat"], select_subprotocol=select_subprotocol_chat + ) + @with_client("/subprotocol", subprotocols=["superchat", "chat"]) + def test_select_subprotocol_argument(self): + server_subprotocol = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_subprotocol, repr("chat")) + self.assertEqual(self.client.subprotocol, "chat") + + class SelectSubprotocolChatServerProtocol(WebSocketServerProtocol): + def select_subprotocol(self, client_subprotocols, server_subprotocols): + return "chat" + + @with_server( + subprotocols=["superchat", "chat"], + create_protocol=SelectSubprotocolChatServerProtocol, + ) + @with_client("/subprotocol", subprotocols=["superchat", "chat"]) + def test_select_subprotocol_override(self): + server_subprotocol = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_subprotocol, repr("chat")) + self.assertEqual(self.client.subprotocol, "chat") + + @with_server() + @with_client("/deprecated_attributes") + def test_protocol_deprecated_attributes(self): + # The test could be connecting with IPv6 or IPv4. + expected_client_attrs = [ + server_socket.getsockname()[:2] + (self.secure,) + for server_socket in self.server.sockets + ] + with warnings.catch_warnings(record=True) as recorded_warnings: + warnings.simplefilter("always") + client_attrs = (self.client.host, self.client.port, self.client.secure) + self.assertDeprecationWarnings( + recorded_warnings, + [ + "use remote_address[0] instead of host", + "use remote_address[1] instead of port", + "don't use secure", + ], + ) + self.assertIn(client_attrs, expected_client_attrs) + + expected_server_attrs = ("localhost", 0, self.secure) + with warnings.catch_warnings(record=True) as recorded_warnings: + warnings.simplefilter("always") + self.loop.run_until_complete(self.client.send("")) + server_attrs = self.loop.run_until_complete(self.client.recv()) + self.assertDeprecationWarnings( + recorded_warnings, + [ + "use local_address[0] instead of host", + "use local_address[1] instead of port", + "don't use secure", + ], + ) + self.assertEqual(server_attrs, repr(expected_server_attrs)) + + @with_server() + @with_client("/path") + def test_protocol_path(self): + client_path = self.client.path + self.assertEqual(client_path, "/path") + server_path = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_path, "/path") + + @with_server() + @with_client("/headers") + def test_protocol_headers(self): + client_req = self.client.request_headers + client_resp = self.client.response_headers + self.assertEqual(client_req["User-Agent"], USER_AGENT) + self.assertEqual(client_resp["Server"], USER_AGENT) + server_req = self.loop.run_until_complete(self.client.recv()) + server_resp = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_req, repr(client_req)) + self.assertEqual(server_resp, repr(client_resp)) + + @with_server() + @with_client("/headers", extra_headers={"X-Spam": "Eggs"}) + def test_protocol_custom_request_headers(self): + req_headers = self.loop.run_until_complete(self.client.recv()) + self.loop.run_until_complete(self.client.recv()) + self.assertIn("('X-Spam', 'Eggs')", req_headers) + + @with_server() + @with_client("/headers", extra_headers={"User-Agent": "websockets"}) + def test_protocol_custom_user_agent_header_legacy(self): + req_headers = self.loop.run_until_complete(self.client.recv()) + self.loop.run_until_complete(self.client.recv()) + self.assertEqual(req_headers.count("User-Agent"), 1) + self.assertIn("('User-Agent', 'websockets')", req_headers) + + @with_server() + @with_client("/headers", user_agent_header=None) + def test_protocol_no_user_agent_header(self): + req_headers = self.loop.run_until_complete(self.client.recv()) + self.loop.run_until_complete(self.client.recv()) + self.assertNotIn("User-Agent", req_headers) + + @with_server() + @with_client("/headers", user_agent_header="websockets") + def test_protocol_custom_user_agent_header(self): + req_headers = self.loop.run_until_complete(self.client.recv()) + self.loop.run_until_complete(self.client.recv()) + self.assertEqual(req_headers.count("User-Agent"), 1) + self.assertIn("('User-Agent', 'websockets')", req_headers) + + @with_server(extra_headers=lambda p, r: {"X-Spam": "Eggs"}) + @with_client("/headers") + def test_protocol_custom_response_headers_callable(self): + self.loop.run_until_complete(self.client.recv()) + resp_headers = self.loop.run_until_complete(self.client.recv()) + self.assertIn("('X-Spam', 'Eggs')", resp_headers) + + @with_server(extra_headers=lambda p, r: None) + @with_client("/headers") + def test_protocol_custom_response_headers_callable_none(self): + self.loop.run_until_complete(self.client.recv()) # doesn't crash + self.loop.run_until_complete(self.client.recv()) # nothing to check + + @with_server(extra_headers={"X-Spam": "Eggs"}) + @with_client("/headers") + def test_protocol_custom_response_headers(self): + self.loop.run_until_complete(self.client.recv()) + resp_headers = self.loop.run_until_complete(self.client.recv()) + self.assertIn("('X-Spam', 'Eggs')", resp_headers) + + @with_server(extra_headers={"Server": "websockets"}) + @with_client("/headers") + def test_protocol_custom_server_header_legacy(self): + self.loop.run_until_complete(self.client.recv()) + resp_headers = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(resp_headers.count("Server"), 1) + self.assertIn("('Server', 'websockets')", resp_headers) + + @with_server(server_header=None) + @with_client("/headers") + def test_protocol_no_server_header(self): + self.loop.run_until_complete(self.client.recv()) + resp_headers = self.loop.run_until_complete(self.client.recv()) + self.assertNotIn("Server", resp_headers) + + @with_server(server_header="websockets") + @with_client("/headers") + def test_protocol_custom_server_header(self): + self.loop.run_until_complete(self.client.recv()) + resp_headers = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(resp_headers.count("Server"), 1) + self.assertIn("('Server', 'websockets')", resp_headers) + + @with_server(create_protocol=HealthCheckServerProtocol) + def test_http_request_http_endpoint(self): + # Making an HTTP request to an HTTP endpoint succeeds. + response = self.loop.run_until_complete(self.make_http_request("/__health__/")) + + with contextlib.closing(response): + self.assertEqual(response.code, 200) + self.assertEqual(response.read(), b"status = green\n") + + @with_server(create_protocol=HealthCheckServerProtocol) + def test_http_request_ws_endpoint(self): + # Making an HTTP request to a WS endpoint fails. + with self.assertRaises(urllib.error.HTTPError) as raised: + self.loop.run_until_complete(self.make_http_request()) + + self.assertEqual(raised.exception.code, 426) + self.assertEqual(raised.exception.headers["Upgrade"], "websocket") + + @with_server(create_protocol=HealthCheckServerProtocol) + def test_ws_connection_http_endpoint(self): + # Making a WS connection to an HTTP endpoint fails. + with self.assertRaises(InvalidStatusCode) as raised: + self.start_client("/__health__/") + + self.assertEqual(raised.exception.status_code, 200) + + @with_server(create_protocol=HealthCheckServerProtocol) + def test_ws_connection_ws_endpoint(self): + # Making a WS connection to a WS endpoint succeeds. + self.start_client() + self.loop.run_until_complete(self.client.send("Hello!")) + self.loop.run_until_complete(self.client.recv()) + self.stop_client() + + @with_server(create_protocol=HealthCheckServerProtocol, server_header=None) + def test_http_request_no_server_header(self): + response = self.loop.run_until_complete(self.make_http_request("/__health__/")) + + with contextlib.closing(response): + self.assertNotIn("Server", response.headers) + + @with_server(create_protocol=HealthCheckServerProtocol, server_header="websockets") + def test_http_request_custom_server_header(self): + response = self.loop.run_until_complete(self.make_http_request("/__health__/")) + + with contextlib.closing(response): + self.assertEqual(response.headers["Server"], "websockets") + + @with_server(create_protocol=ProcessRequestReturningIntProtocol) + def test_process_request_returns_int_status(self): + response = self.loop.run_until_complete(self.make_http_request("/__health__/")) + + with contextlib.closing(response): + self.assertEqual(response.code, 200) + self.assertEqual(response.read(), b"OK\n") + + def assert_client_raises_code(self, status_code): + with self.assertRaises(InvalidStatusCode) as raised: + self.start_client() + self.assertEqual(raised.exception.status_code, status_code) + + @with_server(create_protocol=UnauthorizedServerProtocol) + def test_server_create_protocol(self): + self.assert_client_raises_code(401) + + def create_unauthorized_server_protocol(*args, **kwargs): + return UnauthorizedServerProtocol(*args, **kwargs) + + @with_server(create_protocol=create_unauthorized_server_protocol) + def test_server_create_protocol_function(self): + self.assert_client_raises_code(401) + + @with_server( + klass=UnauthorizedServerProtocol, + deprecation_warnings=["rename klass to create_protocol"], + ) + def test_server_klass_backwards_compatibility(self): + self.assert_client_raises_code(401) + + @with_server( + create_protocol=ForbiddenServerProtocol, + klass=UnauthorizedServerProtocol, + deprecation_warnings=["rename klass to create_protocol"], + ) + def test_server_create_protocol_over_klass(self): + self.assert_client_raises_code(403) + + @with_server() + @with_client("/path", create_protocol=FooClientProtocol) + def test_client_create_protocol(self): + self.assertIsInstance(self.client, FooClientProtocol) + + @with_server() + @with_client( + "/path", + create_protocol=(lambda *args, **kwargs: FooClientProtocol(*args, **kwargs)), + ) + def test_client_create_protocol_function(self): + self.assertIsInstance(self.client, FooClientProtocol) + + @with_server() + @with_client( + "/path", + klass=FooClientProtocol, + deprecation_warnings=["rename klass to create_protocol"], + ) + def test_client_klass(self): + self.assertIsInstance(self.client, FooClientProtocol) + + @with_server() + @with_client( + "/path", + create_protocol=BarClientProtocol, + klass=FooClientProtocol, + deprecation_warnings=["rename klass to create_protocol"], + ) + def test_client_create_protocol_over_klass(self): + self.assertIsInstance(self.client, BarClientProtocol) + + @with_server(close_timeout=7) + @with_client("/close_timeout") + def test_server_close_timeout(self): + close_timeout = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(eval(close_timeout), 7) + + @with_server(timeout=6, deprecation_warnings=["rename timeout to close_timeout"]) + @with_client("/close_timeout") + def test_server_timeout_backwards_compatibility(self): + close_timeout = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(eval(close_timeout), 6) + + @with_server( + close_timeout=7, + timeout=6, + deprecation_warnings=["rename timeout to close_timeout"], + ) + @with_client("/close_timeout") + def test_server_close_timeout_over_timeout(self): + close_timeout = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(eval(close_timeout), 7) + + @with_server() + @with_client("/close_timeout", close_timeout=7) + def test_client_close_timeout(self): + self.assertEqual(self.client.close_timeout, 7) + + @with_server() + @with_client( + "/close_timeout", + timeout=6, + deprecation_warnings=["rename timeout to close_timeout"], + ) + def test_client_timeout_backwards_compatibility(self): + self.assertEqual(self.client.close_timeout, 6) + + @with_server() + @with_client( + "/close_timeout", + close_timeout=7, + timeout=6, + deprecation_warnings=["rename timeout to close_timeout"], + ) + def test_client_close_timeout_over_timeout(self): + self.assertEqual(self.client.close_timeout, 7) + + @with_server() + @with_client("/extensions") + def test_no_extension(self): + server_extensions = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_extensions, repr([])) + self.assertEqual(repr(self.client.extensions), repr([])) + + @with_server(extensions=[ServerNoOpExtensionFactory()]) + @with_client("/extensions", extensions=[ClientNoOpExtensionFactory()]) + def test_extension(self): + server_extensions = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_extensions, repr([NoOpExtension()])) + self.assertEqual(repr(self.client.extensions), repr([NoOpExtension()])) + + @with_server() + @with_client("/extensions", extensions=[ClientNoOpExtensionFactory()]) + def test_extension_not_accepted(self): + server_extensions = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_extensions, repr([])) + self.assertEqual(repr(self.client.extensions), repr([])) + + @with_server(extensions=[ServerNoOpExtensionFactory()]) + @with_client("/extensions") + def test_extension_not_requested(self): + server_extensions = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_extensions, repr([])) + self.assertEqual(repr(self.client.extensions), repr([])) + + @with_server(extensions=[ServerNoOpExtensionFactory([("foo", None)])]) + def test_extension_client_rejection(self): + with self.assertRaises(NegotiationError): + self.start_client("/extensions", extensions=[ClientNoOpExtensionFactory()]) + + @with_server( + extensions=[ + # No match because the client doesn't send client_max_window_bits. + ServerPerMessageDeflateFactory( + client_max_window_bits=10, + require_client_max_window_bits=True, + ), + ServerPerMessageDeflateFactory(), + ] + ) + @with_client( + "/extensions", + extensions=[ + ClientPerMessageDeflateFactory(client_max_window_bits=None), + ], + ) + def test_extension_no_match_then_match(self): + # The order requested by the client has priority. + server_extensions = self.loop.run_until_complete(self.client.recv()) + self.assertEqual( + server_extensions, repr([PerMessageDeflate(False, False, 15, 15)]) + ) + self.assertEqual( + repr(self.client.extensions), + repr([PerMessageDeflate(False, False, 15, 15)]), + ) + + @with_server(extensions=[ServerPerMessageDeflateFactory()]) + @with_client("/extensions", extensions=[ClientNoOpExtensionFactory()]) + def test_extension_mismatch(self): + server_extensions = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_extensions, repr([])) + self.assertEqual(repr(self.client.extensions), repr([])) + + @with_server( + extensions=[ServerNoOpExtensionFactory(), ServerPerMessageDeflateFactory()] + ) + @with_client( + "/extensions", + extensions=[ClientPerMessageDeflateFactory(), ClientNoOpExtensionFactory()], + ) + def test_extension_order(self): + # The order requested by the client has priority. + server_extensions = self.loop.run_until_complete(self.client.recv()) + self.assertEqual( + server_extensions, + repr([PerMessageDeflate(False, False, 15, 15), NoOpExtension()]), + ) + self.assertEqual( + repr(self.client.extensions), + repr([PerMessageDeflate(False, False, 15, 15), NoOpExtension()]), + ) + + @with_server(extensions=[ServerNoOpExtensionFactory()]) + @unittest.mock.patch.object(WebSocketServerProtocol, "process_extensions") + def test_extensions_error(self, _process_extensions): + _process_extensions.return_value = "x-no-op", [NoOpExtension()] + + with self.assertRaises(NegotiationError): + self.start_client( + "/extensions", extensions=[ClientPerMessageDeflateFactory()] + ) + + @with_server(extensions=[ServerNoOpExtensionFactory()]) + @unittest.mock.patch.object(WebSocketServerProtocol, "process_extensions") + def test_extensions_error_no_extensions(self, _process_extensions): + _process_extensions.return_value = "x-no-op", [NoOpExtension()] + + with self.assertRaises(InvalidHandshake): + self.start_client("/extensions") + + @with_server(compression="deflate") + @with_client("/extensions", compression="deflate") + def test_compression_deflate(self): + server_extensions = self.loop.run_until_complete(self.client.recv()) + self.assertEqual( + server_extensions, repr([PerMessageDeflate(False, False, 12, 12)]) + ) + self.assertEqual( + repr(self.client.extensions), + repr([PerMessageDeflate(False, False, 12, 12)]), + ) + + def test_compression_unsupported_server(self): + with self.assertRaises(ValueError): + self.start_server(compression="xz") + + @with_server() + def test_compression_unsupported_client(self): + with self.assertRaises(ValueError): + self.start_client(compression="xz") + + @with_server() + @with_client("/subprotocol") + def test_no_subprotocol(self): + server_subprotocol = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_subprotocol, repr(None)) + self.assertEqual(self.client.subprotocol, None) + + @with_server(subprotocols=["superchat", "chat"]) + @with_client("/subprotocol", subprotocols=["otherchat", "chat"]) + def test_subprotocol(self): + server_subprotocol = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_subprotocol, repr("chat")) + self.assertEqual(self.client.subprotocol, "chat") + + def test_invalid_subprotocol_server(self): + with self.assertRaises(TypeError): + self.start_server(subprotocols="sip") + + @with_server() + def test_invalid_subprotocol_client(self): + with self.assertRaises(TypeError): + self.start_client(subprotocols="sip") + + @with_server(subprotocols=["superchat"]) + @with_client("/subprotocol", subprotocols=["otherchat"]) + def test_subprotocol_not_accepted(self): + server_subprotocol = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_subprotocol, repr(None)) + self.assertEqual(self.client.subprotocol, None) + + @with_server() + @with_client("/subprotocol", subprotocols=["otherchat", "chat"]) + def test_subprotocol_not_offered(self): + server_subprotocol = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_subprotocol, repr(None)) + self.assertEqual(self.client.subprotocol, None) + + @with_server(subprotocols=["superchat", "chat"]) + @with_client("/subprotocol") + def test_subprotocol_not_requested(self): + server_subprotocol = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(server_subprotocol, repr(None)) + self.assertEqual(self.client.subprotocol, None) + + @with_server(subprotocols=["superchat"]) + @unittest.mock.patch.object(WebSocketServerProtocol, "process_subprotocol") + def test_subprotocol_error(self, _process_subprotocol): + _process_subprotocol.return_value = "superchat" + + with self.assertRaises(NegotiationError): + self.start_client("/subprotocol", subprotocols=["otherchat"]) + self.run_loop_once() + + @with_server(subprotocols=["superchat"]) + @unittest.mock.patch.object(WebSocketServerProtocol, "process_subprotocol") + def test_subprotocol_error_no_subprotocols(self, _process_subprotocol): + _process_subprotocol.return_value = "superchat" + + with self.assertRaises(InvalidHandshake): + self.start_client("/subprotocol") + self.run_loop_once() + + @with_server(subprotocols=["superchat", "chat"]) + @unittest.mock.patch.object(WebSocketServerProtocol, "process_subprotocol") + def test_subprotocol_error_two_subprotocols(self, _process_subprotocol): + _process_subprotocol.return_value = "superchat, chat" + + with self.assertRaises(InvalidHandshake): + self.start_client("/subprotocol", subprotocols=["superchat", "chat"]) + self.run_loop_once() + + @with_server() + @unittest.mock.patch("websockets.legacy.server.read_request") + def test_server_receives_malformed_request(self, _read_request): + _read_request.side_effect = ValueError("read_request failed") + + with self.assertRaises(InvalidHandshake): + self.start_client() + + @with_server() + @unittest.mock.patch("websockets.legacy.client.read_response") + def test_client_receives_malformed_response(self, _read_response): + _read_response.side_effect = ValueError("read_response failed") + + with self.assertRaises(InvalidHandshake): + self.start_client() + self.run_loop_once() + + @with_server() + @unittest.mock.patch("websockets.legacy.client.build_request") + def test_client_sends_invalid_handshake_request(self, _build_request): + def wrong_build_request(headers): + return "42" + + _build_request.side_effect = wrong_build_request + + with self.assertRaises(InvalidHandshake): + self.start_client() + + @with_server() + @unittest.mock.patch("websockets.legacy.server.build_response") + def test_server_sends_invalid_handshake_response(self, _build_response): + def wrong_build_response(headers, key): + return build_response(headers, "42") + + _build_response.side_effect = wrong_build_response + + with self.assertRaises(InvalidHandshake): + self.start_client() + + @with_server() + @unittest.mock.patch("websockets.legacy.client.read_response") + def test_server_does_not_switch_protocols(self, _read_response): + async def wrong_read_response(stream): + status_code, reason, headers = await read_response(stream) + return 400, "Bad Request", headers + + _read_response.side_effect = wrong_read_response + + with self.assertRaises(InvalidStatusCode): + self.start_client() + self.run_loop_once() + + @with_server() + @unittest.mock.patch( + "websockets.legacy.server.WebSocketServerProtocol.process_request" + ) + def test_server_error_in_handshake(self, _process_request): + _process_request.side_effect = Exception("process_request crashed") + + with self.assertRaises(InvalidHandshake): + self.start_client() + + @with_server(create_protocol=SlowOpeningHandshakeProtocol) + def test_client_connect_canceled_during_handshake(self): + sock = socket.create_connection(get_server_address(self.server)) + sock.send(b"") # socket is connected + + async def cancelled_client(): + start_client = connect(get_server_uri(self.server), sock=sock) + async with asyncio_timeout(5 * MS): + await start_client + + with self.assertRaises(asyncio.TimeoutError): + self.loop.run_until_complete(cancelled_client()) + + with self.assertRaises(OSError): + sock.send(b"") # socket is closed + + @with_server() + @unittest.mock.patch("websockets.legacy.server.WebSocketServerProtocol.send") + def test_server_handler_crashes(self, send): + send.side_effect = ValueError("send failed") + + with self.temp_client(): + self.loop.run_until_complete(self.client.send("Hello!")) + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.client.recv()) + + # Connection ends with an unexpected error. + self.assertEqual(self.client.close_code, CloseCode.INTERNAL_ERROR) + + @with_server() + @unittest.mock.patch("websockets.legacy.server.WebSocketServerProtocol.close") + def test_server_close_crashes(self, close): + close.side_effect = ValueError("close failed") + + with self.temp_client(): + self.loop.run_until_complete(self.client.send("Hello!")) + reply = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(reply, "Hello!") + + # Connection ends with an abnormal closure. + self.assertEqual(self.client.close_code, CloseCode.ABNORMAL_CLOSURE) + + @with_server() + @with_client() + @unittest.mock.patch.object(WebSocketClientProtocol, "handshake") + def test_client_closes_connection_before_handshake(self, handshake): + # We have mocked the handshake() method to prevent the client from + # performing the opening handshake. Force it to close the connection. + self.client.transport.close() + # The server should stop properly anyway. It used to hang because the + # task handling the connection was waiting for the opening handshake. + + @with_server(create_protocol=SlowOpeningHandshakeProtocol) + def test_server_shuts_down_during_opening_handshake(self): + self.loop.call_later(5 * MS, self.server.close) + with self.assertRaises(InvalidStatusCode) as raised: + self.start_client() + exception = raised.exception + self.assertEqual( + str(exception), "server rejected WebSocket connection: HTTP 503" + ) + self.assertEqual(exception.status_code, 503) + + @with_server() + def test_server_shuts_down_during_connection_handling(self): + with self.temp_client(): + server_ws = next(iter(self.server.websockets)) + self.server.close() + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.client.send("Hello!")) + self.loop.run_until_complete(self.client.recv()) + + # Server closed the connection with 1001 Going Away. + self.assertEqual(self.client.close_code, CloseCode.GOING_AWAY) + self.assertEqual(server_ws.close_code, CloseCode.GOING_AWAY) + + @with_server() + def test_server_shuts_down_gracefully_during_connection_handling(self): + with self.temp_client(): + server_ws = next(iter(self.server.websockets)) + self.server.close(close_connections=False) + self.loop.run_until_complete(self.client.send("Hello!")) + self.loop.run_until_complete(self.client.recv()) + + # Client closed the connection with 1000 OK. + self.assertEqual(self.client.close_code, CloseCode.NORMAL_CLOSURE) + self.assertEqual(server_ws.close_code, CloseCode.NORMAL_CLOSURE) + + @with_server() + def test_server_shuts_down_and_waits_until_handlers_terminate(self): + # This handler waits a bit after the connection is closed in order + # to test that wait_closed() really waits for handlers to complete. + self.start_client("/slow_stop") + server_ws = next(iter(self.server.websockets)) + + # Test that the handler task keeps running after close(). + self.server.close() + self.loop.run_until_complete(asyncio.sleep(MS)) + self.assertFalse(server_ws.handler_task.done()) + + # Test that the handler task terminates before wait_closed() returns. + self.loop.run_until_complete(self.server.wait_closed()) + self.assertTrue(server_ws.handler_task.done()) + + @with_server(create_protocol=ForbiddenServerProtocol) + def test_invalid_status_error_during_client_connect(self): + with self.assertRaises(InvalidStatusCode) as raised: + self.start_client() + exception = raised.exception + self.assertEqual( + str(exception), "server rejected WebSocket connection: HTTP 403" + ) + self.assertEqual(exception.status_code, 403) + + @with_server() + @unittest.mock.patch( + "websockets.legacy.server.WebSocketServerProtocol.write_http_response" + ) + @unittest.mock.patch( + "websockets.legacy.server.WebSocketServerProtocol.read_http_request" + ) + def test_connection_error_during_opening_handshake( + self, _read_http_request, _write_http_response + ): + _read_http_request.side_effect = ConnectionError + + # This exception is currently platform-dependent. It was observed to + # be ConnectionResetError on Linux in the non-TLS case, and + # InvalidMessage otherwise (including both Linux and macOS). This + # doesn't matter though since this test is primarily for testing a + # code path on the server side. + with self.assertRaises(Exception): + self.start_client() + + # No response must not be written if the network connection is broken. + _write_http_response.assert_not_called() + + @with_server() + @unittest.mock.patch("websockets.legacy.server.WebSocketServerProtocol.close") + def test_connection_error_during_closing_handshake(self, close): + close.side_effect = ConnectionError + + with self.temp_client(): + self.loop.run_until_complete(self.client.send("Hello!")) + reply = self.loop.run_until_complete(self.client.recv()) + self.assertEqual(reply, "Hello!") + + # Connection ends with an abnormal closure. + self.assertEqual(self.client.close_code, CloseCode.ABNORMAL_CLOSURE) + + +class ClientServerTests( + CommonClientServerTests, ClientServerTestsMixin, AsyncioTestCase +): + pass + + +class SecureClientServerTests( + CommonClientServerTests, SecureClientServerTestsMixin, AsyncioTestCase +): + # The implementation of this test makes it hard to run it over TLS. + test_client_connect_canceled_during_handshake = None + + # TLS over Unix sockets doesn't make sense. + test_unix_socket = None + + # This test fails under PyPy due to a difference with CPython. + if platform.python_implementation() == "PyPy": # pragma: no cover + test_http_request_ws_endpoint = None + + @with_server() + def test_ws_uri_is_rejected(self): + with self.assertRaises(ValueError): + self.start_client( + uri=get_server_uri(self.server, secure=False), ssl=self.client_context + ) + + def test_redirect_insecure(self): + with temp_test_redirecting_server(self): + with self.assertRaises(InvalidHandshake): + with self.temp_client("/force_insecure"): + self.fail("did not raise") + + +class ClientServerOriginTests(ClientServerTestsMixin, AsyncioTestCase): + @with_server(origins=["http://localhost"]) + @with_client(origin="http://localhost") + def test_checking_origin_succeeds(self): + self.loop.run_until_complete(self.client.send("Hello!")) + self.assertEqual(self.loop.run_until_complete(self.client.recv()), "Hello!") + + @with_server(origins=["http://localhost"]) + def test_checking_origin_fails(self): + with self.assertRaisesRegex( + InvalidHandshake, "server rejected WebSocket connection: HTTP 403" + ): + self.start_client(origin="http://otherhost") + + @with_server(origins=["http://localhost"]) + def test_checking_origins_fails_with_multiple_headers(self): + with self.assertRaisesRegex( + InvalidHandshake, "server rejected WebSocket connection: HTTP 400" + ): + self.start_client( + origin="http://localhost", + extra_headers=[("Origin", "http://otherhost")], + ) + + @with_server(origins=[None]) + @with_client() + def test_checking_lack_of_origin_succeeds(self): + self.loop.run_until_complete(self.client.send("Hello!")) + self.assertEqual(self.loop.run_until_complete(self.client.recv()), "Hello!") + + @with_server(origins=[""]) + # The deprecation warning is raised when a client connects to the server. + @with_client(deprecation_warnings=["use None instead of '' in origins"]) + def test_checking_lack_of_origin_succeeds_backwards_compatibility(self): + self.loop.run_until_complete(self.client.send("Hello!")) + self.assertEqual(self.loop.run_until_complete(self.client.recv()), "Hello!") + + +@unittest.skipIf( + sys.version_info[:2] >= (3, 11), "asyncio.coroutine has been removed in Python 3.11" +) +class YieldFromTests(ClientServerTestsMixin, AsyncioTestCase): # pragma: no cover + @with_server() + def test_client(self): + # @asyncio.coroutine is deprecated on Python ≥ 3.8 + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + @asyncio.coroutine + def run_client(): + # Yield from connect. + client = yield from connect(get_server_uri(self.server)) + self.assertEqual(client.state, State.OPEN) + yield from client.close() + self.assertEqual(client.state, State.CLOSED) + + self.loop.run_until_complete(run_client()) + + def test_server(self): + # @asyncio.coroutine is deprecated on Python ≥ 3.8 + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + + @asyncio.coroutine + def run_server(): + # Yield from serve. + server = yield from serve(default_handler, "localhost", 0) + self.assertTrue(server.sockets) + server.close() + yield from server.wait_closed() + self.assertFalse(server.sockets) + + self.loop.run_until_complete(run_server()) + + +class AsyncAwaitTests(ClientServerTestsMixin, AsyncioTestCase): + @with_server() + def test_client(self): + async def run_client(): + # Await connect. + client = await connect(get_server_uri(self.server)) + self.assertEqual(client.state, State.OPEN) + await client.close() + self.assertEqual(client.state, State.CLOSED) + + self.loop.run_until_complete(run_client()) + + def test_server(self): + async def run_server(): + # Await serve. + server = await serve(default_handler, "localhost", 0) + self.assertTrue(server.sockets) + server.close() + await server.wait_closed() + self.assertFalse(server.sockets) + + self.loop.run_until_complete(run_server()) + + +class ContextManagerTests(ClientServerTestsMixin, AsyncioTestCase): + @with_server() + def test_client(self): + async def run_client(): + # Use connect as an asynchronous context manager. + async with connect(get_server_uri(self.server)) as client: + self.assertEqual(client.state, State.OPEN) + + # Check that exiting the context manager closed the connection. + self.assertEqual(client.state, State.CLOSED) + + self.loop.run_until_complete(run_client()) + + def test_server(self): + async def run_server(): + # Use serve as an asynchronous context manager. + async with serve(default_handler, "localhost", 0) as server: + self.assertTrue(server.sockets) + + # Check that exiting the context manager closed the server. + self.assertFalse(server.sockets) + + self.loop.run_until_complete(run_server()) + + @unittest.skipUnless(hasattr(socket, "AF_UNIX"), "this test requires Unix sockets") + def test_unix_server(self): + async def run_server(path): + async with unix_serve(default_handler, path) as server: + self.assertTrue(server.sockets) + + # Check that exiting the context manager closed the server. + self.assertFalse(server.sockets) + + with temp_unix_socket_path() as path: + self.loop.run_until_complete(run_server(path)) + + +class AsyncIteratorTests(ClientServerTestsMixin, AsyncioTestCase): + # This is a protocol-level feature, but since it's a high-level API, it is + # much easier to exercise at the client or server level. + + MESSAGES = ["3", "2", "1", "Fire!"] + + async def echo_handler(ws): + for message in AsyncIteratorTests.MESSAGES: + await ws.send(message) + + @with_server(handler=echo_handler) + def test_iterate_on_messages(self): + messages = [] + + async def run_client(): + nonlocal messages + async with connect(get_server_uri(self.server)) as ws: + async for message in ws: + messages.append(message) + + self.loop.run_until_complete(run_client()) + + self.assertEqual(messages, self.MESSAGES) + + async def echo_handler_going_away(ws): + for message in AsyncIteratorTests.MESSAGES: + await ws.send(message) + await ws.close(CloseCode.GOING_AWAY) + + @with_server(handler=echo_handler_going_away) + def test_iterate_on_messages_going_away_exit_ok(self): + messages = [] + + async def run_client(): + nonlocal messages + async with connect(get_server_uri(self.server)) as ws: + async for message in ws: + messages.append(message) + + self.loop.run_until_complete(run_client()) + + self.assertEqual(messages, self.MESSAGES) + + async def echo_handler_internal_error(ws): + for message in AsyncIteratorTests.MESSAGES: + await ws.send(message) + await ws.close(CloseCode.INTERNAL_ERROR) + + @with_server(handler=echo_handler_internal_error) + def test_iterate_on_messages_internal_error_exit_not_ok(self): + messages = [] + + async def run_client(): + nonlocal messages + async with connect(get_server_uri(self.server)) as ws: + async for message in ws: + messages.append(message) + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(run_client()) + + self.assertEqual(messages, self.MESSAGES) + + +class ReconnectionTests(ClientServerTestsMixin, AsyncioTestCase): + async def echo_handler(ws): + async for msg in ws: + await ws.send(msg) + + service_available = True + + async def maybe_service_unavailable(path, headers): + if not ReconnectionTests.service_available: + return http.HTTPStatus.SERVICE_UNAVAILABLE, [], b"" + + async def disable_server(self, duration): + ReconnectionTests.service_available = False + await asyncio.sleep(duration) + ReconnectionTests.service_available = True + + @with_server(handler=echo_handler, process_request=maybe_service_unavailable) + def test_reconnect(self): + # Big, ugly integration test :-( + + async def run_client(): + iteration = 0 + connect_inst = connect(get_server_uri(self.server)) + connect_inst.BACKOFF_MIN = 10 * MS + connect_inst.BACKOFF_MAX = 99 * MS + connect_inst.BACKOFF_INITIAL = 0 + # coverage has a hard time dealing with this code - I give up. + async for ws in connect_inst: # pragma: no cover + await ws.send("spam") + msg = await ws.recv() + self.assertEqual(msg, "spam") + + iteration += 1 + if iteration == 1: + # Exit block normally. + pass + elif iteration == 2: + # Disable server for a little bit + asyncio.create_task(self.disable_server(50 * MS)) + await asyncio.sleep(0) + elif iteration == 3: + # Exit block after catching connection error. + server_ws = next(iter(self.server.websockets)) + await server_ws.close() + with self.assertRaises(ConnectionClosed): + await ws.recv() + else: + # Exit block with an exception. + raise Exception("BOOM") + pass # work around bug in coverage + + with self.assertLogs("websockets", logging.INFO) as logs: + with self.assertRaisesRegex(Exception, "BOOM"): + self.loop.run_until_complete(run_client()) + + # Iteration 1 + self.assertEqual( + [record.getMessage() for record in logs.records][:2], + [ + "connection open", + "connection closed", + ], + ) + # Iteration 2 + self.assertEqual( + [record.getMessage() for record in logs.records][2:4], + [ + "connection open", + "connection closed", + ], + ) + # Iteration 3 + self.assertEqual( + [record.getMessage() for record in logs.records][4:-1], + [ + "connection rejected (503 Service Unavailable)", + "connection closed", + "! connect failed; reconnecting in 0.0 seconds", + ] + + [ + "connection rejected (503 Service Unavailable)", + "connection closed", + "! connect failed again; retrying in 0 seconds", + ] + * ((len(logs.records) - 8) // 3) + + [ + "connection open", + "connection closed", + ], + ) + # Iteration 4 + self.assertEqual( + [record.getMessage() for record in logs.records][-1:], + [ + "connection open", + ], + ) + + +class LoggerTests(ClientServerTestsMixin, AsyncioTestCase): + def test_logger_client(self): + with self.assertLogs("test.server", logging.DEBUG) as server_logs: + self.start_server(logger=logging.getLogger("test.server")) + with self.assertLogs("test.client", logging.DEBUG) as client_logs: + self.start_client(logger=logging.getLogger("test.client")) + self.loop.run_until_complete(self.client.send("Hello!")) + self.loop.run_until_complete(self.client.recv()) + self.stop_client() + self.stop_server() + + self.assertGreater(len(server_logs.records), 0) + self.assertGreater(len(client_logs.records), 0) diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/legacy/test_framing.py b/tests/wpt/tests/tools/third_party/websockets/tests/legacy/test_framing.py new file mode 100644 index 00000000000..e1e4c891b03 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/legacy/test_framing.py @@ -0,0 +1,206 @@ +import asyncio +import codecs +import dataclasses +import unittest +import unittest.mock +import warnings + +from websockets.exceptions import PayloadTooBig, ProtocolError +from websockets.frames import OP_BINARY, OP_CLOSE, OP_PING, OP_PONG, OP_TEXT, CloseCode +from websockets.legacy.framing import * + +from .utils import AsyncioTestCase + + +class FramingTests(AsyncioTestCase): + def decode(self, message, mask=False, max_size=None, extensions=None): + stream = asyncio.StreamReader(loop=self.loop) + stream.feed_data(message) + stream.feed_eof() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + frame = self.loop.run_until_complete( + Frame.read( + stream.readexactly, + mask=mask, + max_size=max_size, + extensions=extensions, + ) + ) + # Make sure all the data was consumed. + self.assertTrue(stream.at_eof()) + return frame + + def encode(self, frame, mask=False, extensions=None): + write = unittest.mock.Mock() + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + frame.write(write, mask=mask, extensions=extensions) + # Ensure the entire frame is sent with a single call to write(). + # Multiple calls cause TCP fragmentation and degrade performance. + self.assertEqual(write.call_count, 1) + # The frame data is the single positional argument of that call. + self.assertEqual(len(write.call_args[0]), 1) + self.assertEqual(len(write.call_args[1]), 0) + return write.call_args[0][0] + + def round_trip(self, message, expected, mask=False, extensions=None): + decoded = self.decode(message, mask, extensions=extensions) + decoded.check() + self.assertEqual(decoded, expected) + encoded = self.encode(decoded, mask, extensions=extensions) + if mask: # non-deterministic encoding + decoded = self.decode(encoded, mask, extensions=extensions) + self.assertEqual(decoded, expected) + else: # deterministic encoding + self.assertEqual(encoded, message) + + def test_text(self): + self.round_trip(b"\x81\x04Spam", Frame(True, OP_TEXT, b"Spam")) + + def test_text_masked(self): + self.round_trip( + b"\x81\x84\x5b\xfb\xe1\xa8\x08\x8b\x80\xc5", + Frame(True, OP_TEXT, b"Spam"), + mask=True, + ) + + def test_binary(self): + self.round_trip(b"\x82\x04Eggs", Frame(True, OP_BINARY, b"Eggs")) + + def test_binary_masked(self): + self.round_trip( + b"\x82\x84\x53\xcd\xe2\x89\x16\xaa\x85\xfa", + Frame(True, OP_BINARY, b"Eggs"), + mask=True, + ) + + def test_non_ascii_text(self): + self.round_trip( + b"\x81\x05caf\xc3\xa9", Frame(True, OP_TEXT, "café".encode("utf-8")) + ) + + def test_non_ascii_text_masked(self): + self.round_trip( + b"\x81\x85\x64\xbe\xee\x7e\x07\xdf\x88\xbd\xcd", + Frame(True, OP_TEXT, "café".encode("utf-8")), + mask=True, + ) + + def test_close(self): + self.round_trip(b"\x88\x00", Frame(True, OP_CLOSE, b"")) + + def test_ping(self): + self.round_trip(b"\x89\x04ping", Frame(True, OP_PING, b"ping")) + + def test_pong(self): + self.round_trip(b"\x8a\x04pong", Frame(True, OP_PONG, b"pong")) + + def test_long(self): + self.round_trip( + b"\x82\x7e\x00\x7e" + 126 * b"a", Frame(True, OP_BINARY, 126 * b"a") + ) + + def test_very_long(self): + self.round_trip( + b"\x82\x7f\x00\x00\x00\x00\x00\x01\x00\x00" + 65536 * b"a", + Frame(True, OP_BINARY, 65536 * b"a"), + ) + + def test_payload_too_big(self): + with self.assertRaises(PayloadTooBig): + self.decode(b"\x82\x7e\x04\x01" + 1025 * b"a", max_size=1024) + + def test_bad_reserved_bits(self): + for encoded in [b"\xc0\x00", b"\xa0\x00", b"\x90\x00"]: + with self.subTest(encoded=encoded): + with self.assertRaises(ProtocolError): + self.decode(encoded) + + def test_good_opcode(self): + for opcode in list(range(0x00, 0x03)) + list(range(0x08, 0x0B)): + encoded = bytes([0x80 | opcode, 0]) + with self.subTest(encoded=encoded): + self.decode(encoded) # does not raise an exception + + def test_bad_opcode(self): + for opcode in list(range(0x03, 0x08)) + list(range(0x0B, 0x10)): + encoded = bytes([0x80 | opcode, 0]) + with self.subTest(encoded=encoded): + with self.assertRaises(ProtocolError): + self.decode(encoded) + + def test_mask_flag(self): + # Mask flag correctly set. + self.decode(b"\x80\x80\x00\x00\x00\x00", mask=True) + # Mask flag incorrectly unset. + with self.assertRaises(ProtocolError): + self.decode(b"\x80\x80\x00\x00\x00\x00") + # Mask flag correctly unset. + self.decode(b"\x80\x00") + # Mask flag incorrectly set. + with self.assertRaises(ProtocolError): + self.decode(b"\x80\x00", mask=True) + + def test_control_frame_max_length(self): + # At maximum allowed length. + self.decode(b"\x88\x7e\x00\x7d" + 125 * b"a") + # Above maximum allowed length. + with self.assertRaises(ProtocolError): + self.decode(b"\x88\x7e\x00\x7e" + 126 * b"a") + + def test_fragmented_control_frame(self): + # Fin bit correctly set. + self.decode(b"\x88\x00") + # Fin bit incorrectly unset. + with self.assertRaises(ProtocolError): + self.decode(b"\x08\x00") + + def test_extensions(self): + class Rot13: + @staticmethod + def encode(frame): + assert frame.opcode == OP_TEXT + text = frame.data.decode() + data = codecs.encode(text, "rot13").encode() + return dataclasses.replace(frame, data=data) + + # This extensions is symmetrical. + @staticmethod + def decode(frame, *, max_size=None): + return Rot13.encode(frame) + + self.round_trip( + b"\x81\x05uryyb", Frame(True, OP_TEXT, b"hello"), extensions=[Rot13()] + ) + + +class ParseAndSerializeCloseTests(unittest.TestCase): + def assertCloseData(self, code, reason, data): + """ + Serializing code / reason yields data. Parsing data yields code / reason. + + """ + serialized = serialize_close(code, reason) + self.assertEqual(serialized, data) + parsed = parse_close(data) + self.assertEqual(parsed, (code, reason)) + + def test_parse_close_and_serialize_close(self): + self.assertCloseData(CloseCode.NORMAL_CLOSURE, "", b"\x03\xe8") + self.assertCloseData(CloseCode.NORMAL_CLOSURE, "OK", b"\x03\xe8OK") + + def test_parse_close_empty(self): + self.assertEqual(parse_close(b""), (CloseCode.NO_STATUS_RCVD, "")) + + def test_parse_close_errors(self): + with self.assertRaises(ProtocolError): + parse_close(b"\x03") + with self.assertRaises(ProtocolError): + parse_close(b"\x03\xe7") + with self.assertRaises(UnicodeDecodeError): + parse_close(b"\x03\xe8\xff\xff") + + def test_serialize_close_errors(self): + with self.assertRaises(ProtocolError): + serialize_close(999, "") diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/legacy/test_handshake.py b/tests/wpt/tests/tools/third_party/websockets/tests/legacy/test_handshake.py new file mode 100644 index 00000000000..661ae64fc47 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/legacy/test_handshake.py @@ -0,0 +1,184 @@ +import contextlib +import unittest + +from websockets.datastructures import Headers +from websockets.exceptions import ( + InvalidHandshake, + InvalidHeader, + InvalidHeaderValue, + InvalidUpgrade, +) +from websockets.legacy.handshake import * +from websockets.utils import accept_key + + +class HandshakeTests(unittest.TestCase): + def test_round_trip(self): + request_headers = Headers() + request_key = build_request(request_headers) + response_key = check_request(request_headers) + self.assertEqual(request_key, response_key) + response_headers = Headers() + build_response(response_headers, response_key) + check_response(response_headers, request_key) + + @contextlib.contextmanager + def assertValidRequestHeaders(self): + """ + Provide request headers for modification. + + Assert that the transformation kept them valid. + + """ + headers = Headers() + build_request(headers) + yield headers + check_request(headers) + + @contextlib.contextmanager + def assertInvalidRequestHeaders(self, exc_type): + """ + Provide request headers for modification. + + Assert that the transformation made them invalid. + + """ + headers = Headers() + build_request(headers) + yield headers + assert issubclass(exc_type, InvalidHandshake) + with self.assertRaises(exc_type): + check_request(headers) + + def test_request_invalid_connection(self): + with self.assertInvalidRequestHeaders(InvalidUpgrade) as headers: + del headers["Connection"] + headers["Connection"] = "Downgrade" + + def test_request_missing_connection(self): + with self.assertInvalidRequestHeaders(InvalidUpgrade) as headers: + del headers["Connection"] + + def test_request_additional_connection(self): + with self.assertValidRequestHeaders() as headers: + headers["Connection"] = "close" + + def test_request_invalid_upgrade(self): + with self.assertInvalidRequestHeaders(InvalidUpgrade) as headers: + del headers["Upgrade"] + headers["Upgrade"] = "socketweb" + + def test_request_missing_upgrade(self): + with self.assertInvalidRequestHeaders(InvalidUpgrade) as headers: + del headers["Upgrade"] + + def test_request_additional_upgrade(self): + with self.assertInvalidRequestHeaders(InvalidUpgrade) as headers: + headers["Upgrade"] = "socketweb" + + def test_request_invalid_key_not_base64(self): + with self.assertInvalidRequestHeaders(InvalidHeaderValue) as headers: + del headers["Sec-WebSocket-Key"] + headers["Sec-WebSocket-Key"] = "!@#$%^&*()" + + def test_request_invalid_key_not_well_padded(self): + with self.assertInvalidRequestHeaders(InvalidHeaderValue) as headers: + del headers["Sec-WebSocket-Key"] + headers["Sec-WebSocket-Key"] = "CSIRmL8dWYxeAdr/XpEHRw" + + def test_request_invalid_key_not_16_bytes_long(self): + with self.assertInvalidRequestHeaders(InvalidHeaderValue) as headers: + del headers["Sec-WebSocket-Key"] + headers["Sec-WebSocket-Key"] = "ZLpprpvK4PE=" + + def test_request_missing_key(self): + with self.assertInvalidRequestHeaders(InvalidHeader) as headers: + del headers["Sec-WebSocket-Key"] + + def test_request_additional_key(self): + with self.assertInvalidRequestHeaders(InvalidHeader) as headers: + # This duplicates the Sec-WebSocket-Key header. + headers["Sec-WebSocket-Key"] = headers["Sec-WebSocket-Key"] + + def test_request_invalid_version(self): + with self.assertInvalidRequestHeaders(InvalidHeaderValue) as headers: + del headers["Sec-WebSocket-Version"] + headers["Sec-WebSocket-Version"] = "42" + + def test_request_missing_version(self): + with self.assertInvalidRequestHeaders(InvalidHeader) as headers: + del headers["Sec-WebSocket-Version"] + + def test_request_additional_version(self): + with self.assertInvalidRequestHeaders(InvalidHeader) as headers: + # This duplicates the Sec-WebSocket-Version header. + headers["Sec-WebSocket-Version"] = headers["Sec-WebSocket-Version"] + + @contextlib.contextmanager + def assertValidResponseHeaders(self, key="CSIRmL8dWYxeAdr/XpEHRw=="): + """ + Provide response headers for modification. + + Assert that the transformation kept them valid. + + """ + headers = Headers() + build_response(headers, key) + yield headers + check_response(headers, key) + + @contextlib.contextmanager + def assertInvalidResponseHeaders(self, exc_type, key="CSIRmL8dWYxeAdr/XpEHRw=="): + """ + Provide response headers for modification. + + Assert that the transformation made them invalid. + + """ + headers = Headers() + build_response(headers, key) + yield headers + assert issubclass(exc_type, InvalidHandshake) + with self.assertRaises(exc_type): + check_response(headers, key) + + def test_response_invalid_connection(self): + with self.assertInvalidResponseHeaders(InvalidUpgrade) as headers: + del headers["Connection"] + headers["Connection"] = "Downgrade" + + def test_response_missing_connection(self): + with self.assertInvalidResponseHeaders(InvalidUpgrade) as headers: + del headers["Connection"] + + def test_response_additional_connection(self): + with self.assertValidResponseHeaders() as headers: + headers["Connection"] = "close" + + def test_response_invalid_upgrade(self): + with self.assertInvalidResponseHeaders(InvalidUpgrade) as headers: + del headers["Upgrade"] + headers["Upgrade"] = "socketweb" + + def test_response_missing_upgrade(self): + with self.assertInvalidResponseHeaders(InvalidUpgrade) as headers: + del headers["Upgrade"] + + def test_response_additional_upgrade(self): + with self.assertInvalidResponseHeaders(InvalidUpgrade) as headers: + headers["Upgrade"] = "socketweb" + + def test_response_invalid_accept(self): + with self.assertInvalidResponseHeaders(InvalidHeaderValue) as headers: + del headers["Sec-WebSocket-Accept"] + other_key = "1Eq4UDEFQYg3YspNgqxv5g==" + headers["Sec-WebSocket-Accept"] = accept_key(other_key) + + def test_response_missing_accept(self): + with self.assertInvalidResponseHeaders(InvalidHeader) as headers: + del headers["Sec-WebSocket-Accept"] + + def test_response_additional_accept(self): + with self.assertInvalidResponseHeaders(InvalidHeader) as headers: + # This duplicates the Sec-WebSocket-Accept header. + headers["Sec-WebSocket-Accept"] = headers["Sec-WebSocket-Accept"] diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/legacy/test_http.py b/tests/wpt/tests/tools/third_party/websockets/tests/legacy/test_http.py new file mode 100644 index 00000000000..15d53e08d22 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/legacy/test_http.py @@ -0,0 +1,135 @@ +import asyncio + +from websockets.exceptions import SecurityError +from websockets.legacy.http import * +from websockets.legacy.http import read_headers + +from .utils import AsyncioTestCase + + +class HTTPAsyncTests(AsyncioTestCase): + def setUp(self): + super().setUp() + self.stream = asyncio.StreamReader(loop=self.loop) + + async def test_read_request(self): + # Example from the protocol overview in RFC 6455 + self.stream.feed_data( + b"GET /chat HTTP/1.1\r\n" + b"Host: server.example.com\r\n" + b"Upgrade: websocket\r\n" + b"Connection: Upgrade\r\n" + b"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" + b"Origin: http://example.com\r\n" + b"Sec-WebSocket-Protocol: chat, superchat\r\n" + b"Sec-WebSocket-Version: 13\r\n" + b"\r\n" + ) + path, headers = await read_request(self.stream) + self.assertEqual(path, "/chat") + self.assertEqual(headers["Upgrade"], "websocket") + + async def test_read_request_empty(self): + self.stream.feed_eof() + with self.assertRaisesRegex( + EOFError, "connection closed while reading HTTP request line" + ): + await read_request(self.stream) + + async def test_read_request_invalid_request_line(self): + self.stream.feed_data(b"GET /\r\n\r\n") + with self.assertRaisesRegex(ValueError, "invalid HTTP request line: GET /"): + await read_request(self.stream) + + async def test_read_request_unsupported_method(self): + self.stream.feed_data(b"OPTIONS * HTTP/1.1\r\n\r\n") + with self.assertRaisesRegex(ValueError, "unsupported HTTP method: OPTIONS"): + await read_request(self.stream) + + async def test_read_request_unsupported_version(self): + self.stream.feed_data(b"GET /chat HTTP/1.0\r\n\r\n") + with self.assertRaisesRegex(ValueError, "unsupported HTTP version: HTTP/1.0"): + await read_request(self.stream) + + async def test_read_request_invalid_header(self): + self.stream.feed_data(b"GET /chat HTTP/1.1\r\nOops\r\n") + with self.assertRaisesRegex(ValueError, "invalid HTTP header line: Oops"): + await read_request(self.stream) + + async def test_read_response(self): + # Example from the protocol overview in RFC 6455 + self.stream.feed_data( + b"HTTP/1.1 101 Switching Protocols\r\n" + b"Upgrade: websocket\r\n" + b"Connection: Upgrade\r\n" + b"Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" + b"Sec-WebSocket-Protocol: chat\r\n" + b"\r\n" + ) + status_code, reason, headers = await read_response(self.stream) + self.assertEqual(status_code, 101) + self.assertEqual(reason, "Switching Protocols") + self.assertEqual(headers["Upgrade"], "websocket") + + async def test_read_response_empty(self): + self.stream.feed_eof() + with self.assertRaisesRegex( + EOFError, "connection closed while reading HTTP status line" + ): + await read_response(self.stream) + + async def test_read_request_invalid_status_line(self): + self.stream.feed_data(b"Hello!\r\n") + with self.assertRaisesRegex(ValueError, "invalid HTTP status line: Hello!"): + await read_response(self.stream) + + async def test_read_response_unsupported_version(self): + self.stream.feed_data(b"HTTP/1.0 400 Bad Request\r\n\r\n") + with self.assertRaisesRegex(ValueError, "unsupported HTTP version: HTTP/1.0"): + await read_response(self.stream) + + async def test_read_response_invalid_status(self): + self.stream.feed_data(b"HTTP/1.1 OMG WTF\r\n\r\n") + with self.assertRaisesRegex(ValueError, "invalid HTTP status code: OMG"): + await read_response(self.stream) + + async def test_read_response_unsupported_status(self): + self.stream.feed_data(b"HTTP/1.1 007 My name is Bond\r\n\r\n") + with self.assertRaisesRegex(ValueError, "unsupported HTTP status code: 007"): + await read_response(self.stream) + + async def test_read_response_invalid_reason(self): + self.stream.feed_data(b"HTTP/1.1 200 \x7f\r\n\r\n") + with self.assertRaisesRegex(ValueError, "invalid HTTP reason phrase: \\x7f"): + await read_response(self.stream) + + async def test_read_response_invalid_header(self): + self.stream.feed_data(b"HTTP/1.1 500 Internal Server Error\r\nOops\r\n") + with self.assertRaisesRegex(ValueError, "invalid HTTP header line: Oops"): + await read_response(self.stream) + + async def test_header_name(self): + self.stream.feed_data(b"foo bar: baz qux\r\n\r\n") + with self.assertRaises(ValueError): + await read_headers(self.stream) + + async def test_header_value(self): + self.stream.feed_data(b"foo: \x00\x00\x0f\r\n\r\n") + with self.assertRaises(ValueError): + await read_headers(self.stream) + + async def test_headers_limit(self): + self.stream.feed_data(b"foo: bar\r\n" * 129 + b"\r\n") + with self.assertRaises(SecurityError): + await read_headers(self.stream) + + async def test_line_limit(self): + # Header line contains 5 + 8186 + 2 = 8193 bytes. + self.stream.feed_data(b"foo: " + b"a" * 8186 + b"\r\n\r\n") + with self.assertRaises(SecurityError): + await read_headers(self.stream) + + async def test_line_ending(self): + self.stream.feed_data(b"foo: bar\n\n") + with self.assertRaises(EOFError): + await read_headers(self.stream) diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/legacy/test_protocol.py b/tests/wpt/tests/tools/third_party/websockets/tests/legacy/test_protocol.py new file mode 100644 index 00000000000..f2eb0fea03f --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/legacy/test_protocol.py @@ -0,0 +1,1708 @@ +import asyncio +import contextlib +import logging +import sys +import unittest +import unittest.mock +import warnings + +from websockets.exceptions import ConnectionClosed, InvalidState +from websockets.frames import ( + OP_BINARY, + OP_CLOSE, + OP_CONT, + OP_PING, + OP_PONG, + OP_TEXT, + Close, + CloseCode, +) +from websockets.legacy.framing import Frame +from websockets.legacy.protocol import WebSocketCommonProtocol, broadcast +from websockets.protocol import State + +from ..utils import MS +from .utils import AsyncioTestCase + + +async def async_iterable(iterable): + for item in iterable: + yield item + + +class TransportMock(unittest.mock.Mock): + """ + Transport mock to control the protocol's inputs and outputs in tests. + + It calls the protocol's connection_made and connection_lost methods like + actual transports. + + It also calls the protocol's connection_open method to bypass the + WebSocket handshake. + + To simulate incoming data, tests call the protocol's data_received and + eof_received methods directly. + + They could also pause_writing and resume_writing to test flow control. + + """ + + # This should happen in __init__ but overriding Mock.__init__ is hard. + def setup_mock(self, loop, protocol): + self.loop = loop + self.protocol = protocol + self._eof = False + self._closing = False + # Simulate a successful TCP handshake. + self.protocol.connection_made(self) + # Simulate a successful WebSocket handshake. + self.protocol.connection_open() + + def can_write_eof(self): + return True + + def write_eof(self): + # When the protocol half-closes the TCP connection, it expects the + # other end to close it. Simulate that. + if not self._eof: + self.loop.call_soon(self.close) + self._eof = True + + def close(self): + # Simulate how actual transports drop the connection. + if not self._closing: + self.loop.call_soon(self.protocol.connection_lost, None) + self._closing = True + + def abort(self): + # Change this to an `if` if tests call abort() multiple times. + assert self.protocol.state is not State.CLOSED + self.loop.call_soon(self.protocol.connection_lost, None) + + +class CommonTests: + """ + Mixin that defines most tests but doesn't inherit unittest.TestCase. + + Tests are run by the ServerTests and ClientTests subclasses. + + """ + + def setUp(self): + super().setUp() + + # This logic is encapsulated in a coroutine to prevent it from executing + # before the event loop is running which causes asyncio.get_event_loop() + # to raise a DeprecationWarning on Python ≥ 3.10. + + async def create_protocol(): + # Disable pings to make it easier to test what frames are sent exactly. + return WebSocketCommonProtocol(ping_interval=None) + + self.protocol = self.loop.run_until_complete(create_protocol()) + self.transport = TransportMock() + self.transport.setup_mock(self.loop, self.protocol) + + def tearDown(self): + self.transport.close() + self.loop.run_until_complete(self.protocol.close()) + super().tearDown() + + # Utilities for writing tests. + + def make_drain_slow(self, delay=MS): + # Process connection_made in order to initialize self.protocol.transport. + self.run_loop_once() + + original_drain = self.protocol._drain + + async def delayed_drain(): + await asyncio.sleep(delay) + await original_drain() + + self.protocol._drain = delayed_drain + + close_frame = Frame( + True, + OP_CLOSE, + Close(CloseCode.NORMAL_CLOSURE, "close").serialize(), + ) + local_close = Frame( + True, + OP_CLOSE, + Close(CloseCode.NORMAL_CLOSURE, "local").serialize(), + ) + remote_close = Frame( + True, + OP_CLOSE, + Close(CloseCode.NORMAL_CLOSURE, "remote").serialize(), + ) + + def receive_frame(self, frame): + """ + Make the protocol receive a frame. + + """ + write = self.protocol.data_received + mask = not self.protocol.is_client + frame.write(write, mask=mask) + + def receive_eof(self): + """ + Make the protocol receive the end of the data stream. + + Since ``WebSocketCommonProtocol.eof_received`` returns ``None``, an + actual transport would close itself after calling it. This function + emulates that behavior. + + """ + self.protocol.eof_received() + self.loop.call_soon(self.transport.close) + + def receive_eof_if_client(self): + """ + Like receive_eof, but only if this is the client side. + + Since the server is supposed to initiate the termination of the TCP + connection, this method helps making tests work for both sides. + + """ + if self.protocol.is_client: + self.receive_eof() + + def close_connection(self, code=CloseCode.NORMAL_CLOSURE, reason="close"): + """ + Execute a closing handshake. + + This puts the connection in the CLOSED state. + + """ + close_frame_data = Close(code, reason).serialize() + # Prepare the response to the closing handshake from the remote side. + self.receive_frame(Frame(True, OP_CLOSE, close_frame_data)) + self.receive_eof_if_client() + # Trigger the closing handshake from the local side and complete it. + self.loop.run_until_complete(self.protocol.close(code, reason)) + # Empty the outgoing data stream so we can make assertions later on. + self.assertOneFrameSent(True, OP_CLOSE, close_frame_data) + + assert self.protocol.state is State.CLOSED + + def half_close_connection_local( + self, + code=CloseCode.NORMAL_CLOSURE, + reason="close", + ): + """ + Start a closing handshake but do not complete it. + + The main difference with `close_connection` is that the connection is + left in the CLOSING state until the event loop runs again. + + The current implementation returns a task that must be awaited or + canceled, else asyncio complains about destroying a pending task. + + """ + close_frame_data = Close(code, reason).serialize() + # Trigger the closing handshake from the local endpoint. + close_task = self.loop.create_task(self.protocol.close(code, reason)) + self.run_loop_once() # write_frame executes + # Empty the outgoing data stream so we can make assertions later on. + self.assertOneFrameSent(True, OP_CLOSE, close_frame_data) + + assert self.protocol.state is State.CLOSING + + # Complete the closing sequence at 1ms intervals so the test can run + # at each point even it goes back to the event loop several times. + self.loop.call_later( + MS, self.receive_frame, Frame(True, OP_CLOSE, close_frame_data) + ) + self.loop.call_later(2 * MS, self.receive_eof_if_client) + + # This task must be awaited or canceled by the caller. + return close_task + + def half_close_connection_remote( + self, + code=CloseCode.NORMAL_CLOSURE, + reason="close", + ): + """ + Receive a closing handshake but do not complete it. + + The main difference with `close_connection` is that the connection is + left in the CLOSING state until the event loop runs again. + + """ + # On the server side, websockets completes the closing handshake and + # closes the TCP connection immediately. Yield to the event loop after + # sending the close frame to run the test while the connection is in + # the CLOSING state. + if not self.protocol.is_client: + self.make_drain_slow() + + close_frame_data = Close(code, reason).serialize() + # Trigger the closing handshake from the remote endpoint. + self.receive_frame(Frame(True, OP_CLOSE, close_frame_data)) + self.run_loop_once() # read_frame executes + # Empty the outgoing data stream so we can make assertions later on. + self.assertOneFrameSent(True, OP_CLOSE, close_frame_data) + + assert self.protocol.state is State.CLOSING + + # Complete the closing sequence at 1ms intervals so the test can run + # at each point even it goes back to the event loop several times. + self.loop.call_later(2 * MS, self.receive_eof_if_client) + + def process_invalid_frames(self): + """ + Make the protocol fail quickly after simulating invalid data. + + To achieve this, this function triggers the protocol's eof_received, + which interrupts pending reads waiting for more data. + + """ + self.run_loop_once() + self.receive_eof() + self.loop.run_until_complete(self.protocol.close_connection_task) + + def sent_frames(self): + """ + Read all frames sent to the transport. + + """ + stream = asyncio.StreamReader(loop=self.loop) + + for (data,), kw in self.transport.write.call_args_list: + stream.feed_data(data) + self.transport.write.call_args_list = [] + stream.feed_eof() + + frames = [] + while not stream.at_eof(): + frames.append( + self.loop.run_until_complete( + Frame.read(stream.readexactly, mask=self.protocol.is_client) + ) + ) + return frames + + def last_sent_frame(self): + """ + Read the last frame sent to the transport. + + This method assumes that at most one frame was sent. It raises an + AssertionError otherwise. + + """ + frames = self.sent_frames() + if frames: + assert len(frames) == 1 + return frames[0] + + def assertFramesSent(self, *frames): + self.assertEqual(self.sent_frames(), [Frame(*args) for args in frames]) + + def assertOneFrameSent(self, *args): + self.assertEqual(self.last_sent_frame(), Frame(*args)) + + def assertNoFrameSent(self): + self.assertIsNone(self.last_sent_frame()) + + def assertConnectionClosed(self, code, message): + # The following line guarantees that connection_lost was called. + self.assertEqual(self.protocol.state, State.CLOSED) + # A close frame was received. + self.assertEqual(self.protocol.close_code, code) + self.assertEqual(self.protocol.close_reason, message) + + def assertConnectionFailed(self, code, message): + # The following line guarantees that connection_lost was called. + self.assertEqual(self.protocol.state, State.CLOSED) + # No close frame was received. + self.assertEqual(self.protocol.close_code, CloseCode.ABNORMAL_CLOSURE) + self.assertEqual(self.protocol.close_reason, "") + # A close frame was sent -- unless the connection was already lost. + if code == CloseCode.ABNORMAL_CLOSURE: + self.assertNoFrameSent() + else: + self.assertOneFrameSent(True, OP_CLOSE, Close(code, message).serialize()) + + @contextlib.contextmanager + def assertCompletesWithin(self, min_time, max_time): + t0 = self.loop.time() + yield + t1 = self.loop.time() + dt = t1 - t0 + self.assertGreaterEqual(dt, min_time, f"Too fast: {dt} < {min_time}") + self.assertLess(dt, max_time, f"Too slow: {dt} >= {max_time}") + + # Test constructor. + + def test_timeout_backwards_compatibility(self): + async def create_protocol(): + return WebSocketCommonProtocol(ping_interval=None, timeout=5) + + with warnings.catch_warnings(record=True) as recorded: + warnings.simplefilter("always") + protocol = self.loop.run_until_complete(create_protocol()) + + self.assertEqual(protocol.close_timeout, 5) + self.assertDeprecationWarnings(recorded, ["rename timeout to close_timeout"]) + + def test_loop_backwards_compatibility(self): + loop = asyncio.new_event_loop() + self.addCleanup(loop.close) + + with warnings.catch_warnings(record=True) as recorded: + warnings.simplefilter("always") + protocol = WebSocketCommonProtocol(ping_interval=None, loop=loop) + + self.assertEqual(protocol.loop, loop) + self.assertDeprecationWarnings(recorded, ["remove loop argument"]) + + # Test public attributes. + + def test_local_address(self): + get_extra_info = unittest.mock.Mock(return_value=("host", 4312)) + self.transport.get_extra_info = get_extra_info + + self.assertEqual(self.protocol.local_address, ("host", 4312)) + get_extra_info.assert_called_with("sockname") + + def test_local_address_before_connection(self): + # Emulate the situation before connection_open() runs. + _transport = self.protocol.transport + del self.protocol.transport + try: + self.assertEqual(self.protocol.local_address, None) + finally: + self.protocol.transport = _transport + + def test_remote_address(self): + get_extra_info = unittest.mock.Mock(return_value=("host", 4312)) + self.transport.get_extra_info = get_extra_info + + self.assertEqual(self.protocol.remote_address, ("host", 4312)) + get_extra_info.assert_called_with("peername") + + def test_remote_address_before_connection(self): + # Emulate the situation before connection_open() runs. + _transport = self.protocol.transport + del self.protocol.transport + try: + self.assertEqual(self.protocol.remote_address, None) + finally: + self.protocol.transport = _transport + + def test_open(self): + self.assertTrue(self.protocol.open) + self.close_connection() + self.assertFalse(self.protocol.open) + + def test_closed(self): + self.assertFalse(self.protocol.closed) + self.close_connection() + self.assertTrue(self.protocol.closed) + + def test_wait_closed(self): + wait_closed = self.loop.create_task(self.protocol.wait_closed()) + self.assertFalse(wait_closed.done()) + self.close_connection() + self.assertTrue(wait_closed.done()) + + def test_close_code(self): + self.close_connection(CloseCode.GOING_AWAY, "Bye!") + self.assertEqual(self.protocol.close_code, CloseCode.GOING_AWAY) + + def test_close_reason(self): + self.close_connection(CloseCode.GOING_AWAY, "Bye!") + self.assertEqual(self.protocol.close_reason, "Bye!") + + def test_close_code_not_set(self): + self.assertIsNone(self.protocol.close_code) + + def test_close_reason_not_set(self): + self.assertIsNone(self.protocol.close_reason) + + # Test the recv coroutine. + + def test_recv_text(self): + self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8"))) + data = self.loop.run_until_complete(self.protocol.recv()) + self.assertEqual(data, "café") + + def test_recv_binary(self): + self.receive_frame(Frame(True, OP_BINARY, b"tea")) + data = self.loop.run_until_complete(self.protocol.recv()) + self.assertEqual(data, b"tea") + + def test_recv_on_closing_connection_local(self): + close_task = self.half_close_connection_local() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.recv()) + + self.loop.run_until_complete(close_task) # cleanup + + def test_recv_on_closing_connection_remote(self): + self.half_close_connection_remote() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.recv()) + + def test_recv_on_closed_connection(self): + self.close_connection() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.recv()) + + def test_recv_protocol_error(self): + self.receive_frame(Frame(True, OP_CONT, "café".encode("utf-8"))) + self.process_invalid_frames() + self.assertConnectionFailed(CloseCode.PROTOCOL_ERROR, "") + + def test_recv_unicode_error(self): + self.receive_frame(Frame(True, OP_TEXT, "café".encode("latin-1"))) + self.process_invalid_frames() + self.assertConnectionFailed(CloseCode.INVALID_DATA, "") + + def test_recv_text_payload_too_big(self): + self.protocol.max_size = 1024 + self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8") * 205)) + self.process_invalid_frames() + self.assertConnectionFailed(CloseCode.MESSAGE_TOO_BIG, "") + + def test_recv_binary_payload_too_big(self): + self.protocol.max_size = 1024 + self.receive_frame(Frame(True, OP_BINARY, b"tea" * 342)) + self.process_invalid_frames() + self.assertConnectionFailed(CloseCode.MESSAGE_TOO_BIG, "") + + def test_recv_text_no_max_size(self): + self.protocol.max_size = None # for test coverage + self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8") * 205)) + data = self.loop.run_until_complete(self.protocol.recv()) + self.assertEqual(data, "café" * 205) + + def test_recv_binary_no_max_size(self): + self.protocol.max_size = None # for test coverage + self.receive_frame(Frame(True, OP_BINARY, b"tea" * 342)) + data = self.loop.run_until_complete(self.protocol.recv()) + self.assertEqual(data, b"tea" * 342) + + def test_recv_queue_empty(self): + recv = self.loop.create_task(self.protocol.recv()) + with self.assertRaises(asyncio.TimeoutError): + self.loop.run_until_complete( + asyncio.wait_for(asyncio.shield(recv), timeout=MS) + ) + + self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8"))) + data = self.loop.run_until_complete(recv) + self.assertEqual(data, "café") + + def test_recv_queue_full(self): + self.protocol.max_queue = 2 + # Test internals because it's hard to verify buffers from the outside. + self.assertEqual(list(self.protocol.messages), []) + + self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8"))) + self.run_loop_once() + self.assertEqual(list(self.protocol.messages), ["café"]) + + self.receive_frame(Frame(True, OP_BINARY, b"tea")) + self.run_loop_once() + self.assertEqual(list(self.protocol.messages), ["café", b"tea"]) + + self.receive_frame(Frame(True, OP_BINARY, b"milk")) + self.run_loop_once() + self.assertEqual(list(self.protocol.messages), ["café", b"tea"]) + + self.loop.run_until_complete(self.protocol.recv()) + self.run_loop_once() + self.assertEqual(list(self.protocol.messages), [b"tea", b"milk"]) + + self.loop.run_until_complete(self.protocol.recv()) + self.run_loop_once() + self.assertEqual(list(self.protocol.messages), [b"milk"]) + + self.loop.run_until_complete(self.protocol.recv()) + self.run_loop_once() + self.assertEqual(list(self.protocol.messages), []) + + def test_recv_queue_no_limit(self): + self.protocol.max_queue = None + + for _ in range(100): + self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8"))) + self.run_loop_once() + + # Incoming message queue can contain at least 100 messages. + self.assertEqual(list(self.protocol.messages), ["café"] * 100) + + for _ in range(100): + self.loop.run_until_complete(self.protocol.recv()) + + self.assertEqual(list(self.protocol.messages), []) + + def test_recv_other_error(self): + async def read_message(): + raise Exception("BOOM") + + self.protocol.read_message = read_message + self.process_invalid_frames() + self.assertConnectionFailed(CloseCode.INTERNAL_ERROR, "") + + def test_recv_canceled(self): + recv = self.loop.create_task(self.protocol.recv()) + self.loop.call_soon(recv.cancel) + + with self.assertRaises(asyncio.CancelledError): + self.loop.run_until_complete(recv) + + # The next frame doesn't disappear in a vacuum (it used to). + self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8"))) + data = self.loop.run_until_complete(self.protocol.recv()) + self.assertEqual(data, "café") + + def test_recv_canceled_race_condition(self): + recv = self.loop.create_task( + asyncio.wait_for(self.protocol.recv(), timeout=0.000_001) + ) + self.loop.call_soon( + self.receive_frame, Frame(True, OP_TEXT, "café".encode("utf-8")) + ) + + with self.assertRaises(asyncio.TimeoutError): + self.loop.run_until_complete(recv) + + # The previous frame doesn't disappear in a vacuum (it used to). + self.receive_frame(Frame(True, OP_TEXT, "tea".encode("utf-8"))) + data = self.loop.run_until_complete(self.protocol.recv()) + # If we're getting "tea" there, it means "café" was swallowed (ha, ha). + self.assertEqual(data, "café") + + def test_recv_when_transfer_data_cancelled(self): + # Clog incoming queue. + self.protocol.max_queue = 1 + self.receive_frame(Frame(True, OP_TEXT, "café".encode("utf-8"))) + self.receive_frame(Frame(True, OP_BINARY, b"tea")) + self.run_loop_once() + + # Flow control kicks in (check with an implementation detail). + self.assertFalse(self.protocol._put_message_waiter.done()) + + # Schedule recv(). + recv = self.loop.create_task(self.protocol.recv()) + + # Cancel transfer_data_task (again, implementation detail). + self.protocol.fail_connection() + self.run_loop_once() + self.assertTrue(self.protocol.transfer_data_task.cancelled()) + + # recv() completes properly. + self.assertEqual(self.loop.run_until_complete(recv), "café") + + def test_recv_prevents_concurrent_calls(self): + recv = self.loop.create_task(self.protocol.recv()) + + with self.assertRaises(RuntimeError) as raised: + self.loop.run_until_complete(self.protocol.recv()) + self.assertEqual( + str(raised.exception), + "cannot call recv while another coroutine " + "is already waiting for the next message", + ) + recv.cancel() + + # Test the send coroutine. + + def test_send_text(self): + self.loop.run_until_complete(self.protocol.send("café")) + self.assertOneFrameSent(True, OP_TEXT, "café".encode("utf-8")) + + def test_send_binary(self): + self.loop.run_until_complete(self.protocol.send(b"tea")) + self.assertOneFrameSent(True, OP_BINARY, b"tea") + + def test_send_binary_from_bytearray(self): + self.loop.run_until_complete(self.protocol.send(bytearray(b"tea"))) + self.assertOneFrameSent(True, OP_BINARY, b"tea") + + def test_send_binary_from_memoryview(self): + self.loop.run_until_complete(self.protocol.send(memoryview(b"tea"))) + self.assertOneFrameSent(True, OP_BINARY, b"tea") + + def test_send_dict(self): + with self.assertRaises(TypeError): + self.loop.run_until_complete(self.protocol.send({"not": "encoded"})) + self.assertNoFrameSent() + + def test_send_type_error(self): + with self.assertRaises(TypeError): + self.loop.run_until_complete(self.protocol.send(42)) + self.assertNoFrameSent() + + def test_send_iterable_text(self): + self.loop.run_until_complete(self.protocol.send(["ca", "fé"])) + self.assertFramesSent( + (False, OP_TEXT, "ca".encode("utf-8")), + (False, OP_CONT, "fé".encode("utf-8")), + (True, OP_CONT, "".encode("utf-8")), + ) + + def test_send_iterable_binary(self): + self.loop.run_until_complete(self.protocol.send([b"te", b"a"])) + self.assertFramesSent( + (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"") + ) + + def test_send_iterable_binary_from_bytearray(self): + self.loop.run_until_complete( + self.protocol.send([bytearray(b"te"), bytearray(b"a")]) + ) + self.assertFramesSent( + (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"") + ) + + def test_send_iterable_binary_from_memoryview(self): + self.loop.run_until_complete( + self.protocol.send([memoryview(b"te"), memoryview(b"a")]) + ) + self.assertFramesSent( + (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"") + ) + + def test_send_empty_iterable(self): + self.loop.run_until_complete(self.protocol.send([])) + self.assertNoFrameSent() + + def test_send_iterable_type_error(self): + with self.assertRaises(TypeError): + self.loop.run_until_complete(self.protocol.send([42])) + self.assertNoFrameSent() + + def test_send_iterable_mixed_type_error(self): + with self.assertRaises(TypeError): + self.loop.run_until_complete(self.protocol.send(["café", b"tea"])) + self.assertFramesSent( + (False, OP_TEXT, "café".encode("utf-8")), + (True, OP_CLOSE, Close(CloseCode.INTERNAL_ERROR, "").serialize()), + ) + + def test_send_iterable_prevents_concurrent_send(self): + self.make_drain_slow(2 * MS) + + async def send_iterable(): + await self.protocol.send(["ca", "fé"]) + + async def send_concurrent(): + await asyncio.sleep(MS) + await self.protocol.send(b"tea") + + async def run_concurrently(): + await asyncio.gather( + send_iterable(), + send_concurrent(), + ) + + self.loop.run_until_complete(run_concurrently()) + + self.assertFramesSent( + (False, OP_TEXT, "ca".encode("utf-8")), + (False, OP_CONT, "fé".encode("utf-8")), + (True, OP_CONT, "".encode("utf-8")), + (True, OP_BINARY, b"tea"), + ) + + def test_send_async_iterable_text(self): + self.loop.run_until_complete(self.protocol.send(async_iterable(["ca", "fé"]))) + self.assertFramesSent( + (False, OP_TEXT, "ca".encode("utf-8")), + (False, OP_CONT, "fé".encode("utf-8")), + (True, OP_CONT, "".encode("utf-8")), + ) + + def test_send_async_iterable_binary(self): + self.loop.run_until_complete(self.protocol.send(async_iterable([b"te", b"a"]))) + self.assertFramesSent( + (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"") + ) + + def test_send_async_iterable_binary_from_bytearray(self): + self.loop.run_until_complete( + self.protocol.send(async_iterable([bytearray(b"te"), bytearray(b"a")])) + ) + self.assertFramesSent( + (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"") + ) + + def test_send_async_iterable_binary_from_memoryview(self): + self.loop.run_until_complete( + self.protocol.send(async_iterable([memoryview(b"te"), memoryview(b"a")])) + ) + self.assertFramesSent( + (False, OP_BINARY, b"te"), (False, OP_CONT, b"a"), (True, OP_CONT, b"") + ) + + def test_send_empty_async_iterable(self): + self.loop.run_until_complete(self.protocol.send(async_iterable([]))) + self.assertNoFrameSent() + + def test_send_async_iterable_type_error(self): + with self.assertRaises(TypeError): + self.loop.run_until_complete(self.protocol.send(async_iterable([42]))) + self.assertNoFrameSent() + + def test_send_async_iterable_mixed_type_error(self): + with self.assertRaises(TypeError): + self.loop.run_until_complete( + self.protocol.send(async_iterable(["café", b"tea"])) + ) + self.assertFramesSent( + (False, OP_TEXT, "café".encode("utf-8")), + (True, OP_CLOSE, Close(CloseCode.INTERNAL_ERROR, "").serialize()), + ) + + def test_send_async_iterable_prevents_concurrent_send(self): + self.make_drain_slow(2 * MS) + + async def send_async_iterable(): + await self.protocol.send(async_iterable(["ca", "fé"])) + + async def send_concurrent(): + await asyncio.sleep(MS) + await self.protocol.send(b"tea") + + async def run_concurrently(): + await asyncio.gather( + send_async_iterable(), + send_concurrent(), + ) + + self.loop.run_until_complete(run_concurrently()) + + self.assertFramesSent( + (False, OP_TEXT, "ca".encode("utf-8")), + (False, OP_CONT, "fé".encode("utf-8")), + (True, OP_CONT, "".encode("utf-8")), + (True, OP_BINARY, b"tea"), + ) + + def test_send_on_closing_connection_local(self): + close_task = self.half_close_connection_local() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.send("foobar")) + + self.assertNoFrameSent() + + self.loop.run_until_complete(close_task) # cleanup + + def test_send_on_closing_connection_remote(self): + self.half_close_connection_remote() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.send("foobar")) + + self.assertNoFrameSent() + + def test_send_on_closed_connection(self): + self.close_connection() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.send("foobar")) + + self.assertNoFrameSent() + + # Test the ping coroutine. + + def test_ping_default(self): + self.loop.run_until_complete(self.protocol.ping()) + # With our testing tools, it's more convenient to extract the expected + # ping data from the library's internals than from the frame sent. + ping_data = next(iter(self.protocol.pings)) + self.assertIsInstance(ping_data, bytes) + self.assertEqual(len(ping_data), 4) + self.assertOneFrameSent(True, OP_PING, ping_data) + + def test_ping_text(self): + self.loop.run_until_complete(self.protocol.ping("café")) + self.assertOneFrameSent(True, OP_PING, "café".encode("utf-8")) + + def test_ping_binary(self): + self.loop.run_until_complete(self.protocol.ping(b"tea")) + self.assertOneFrameSent(True, OP_PING, b"tea") + + def test_ping_binary_from_bytearray(self): + self.loop.run_until_complete(self.protocol.ping(bytearray(b"tea"))) + self.assertOneFrameSent(True, OP_PING, b"tea") + + def test_ping_binary_from_memoryview(self): + self.loop.run_until_complete(self.protocol.ping(memoryview(b"tea"))) + self.assertOneFrameSent(True, OP_PING, b"tea") + + def test_ping_type_error(self): + with self.assertRaises(TypeError): + self.loop.run_until_complete(self.protocol.ping(42)) + self.assertNoFrameSent() + + def test_ping_on_closing_connection_local(self): + close_task = self.half_close_connection_local() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.ping()) + + self.assertNoFrameSent() + + self.loop.run_until_complete(close_task) # cleanup + + def test_ping_on_closing_connection_remote(self): + self.half_close_connection_remote() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.ping()) + + self.assertNoFrameSent() + + def test_ping_on_closed_connection(self): + self.close_connection() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.ping()) + + self.assertNoFrameSent() + + # Test the pong coroutine. + + def test_pong_default(self): + self.loop.run_until_complete(self.protocol.pong()) + self.assertOneFrameSent(True, OP_PONG, b"") + + def test_pong_text(self): + self.loop.run_until_complete(self.protocol.pong("café")) + self.assertOneFrameSent(True, OP_PONG, "café".encode("utf-8")) + + def test_pong_binary(self): + self.loop.run_until_complete(self.protocol.pong(b"tea")) + self.assertOneFrameSent(True, OP_PONG, b"tea") + + def test_pong_binary_from_bytearray(self): + self.loop.run_until_complete(self.protocol.pong(bytearray(b"tea"))) + self.assertOneFrameSent(True, OP_PONG, b"tea") + + def test_pong_binary_from_memoryview(self): + self.loop.run_until_complete(self.protocol.pong(memoryview(b"tea"))) + self.assertOneFrameSent(True, OP_PONG, b"tea") + + def test_pong_type_error(self): + with self.assertRaises(TypeError): + self.loop.run_until_complete(self.protocol.pong(42)) + self.assertNoFrameSent() + + def test_pong_on_closing_connection_local(self): + close_task = self.half_close_connection_local() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.pong()) + + self.assertNoFrameSent() + + self.loop.run_until_complete(close_task) # cleanup + + def test_pong_on_closing_connection_remote(self): + self.half_close_connection_remote() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.pong()) + + self.assertNoFrameSent() + + def test_pong_on_closed_connection(self): + self.close_connection() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.pong()) + + self.assertNoFrameSent() + + # Test the protocol's logic for acknowledging pings with pongs. + + def test_answer_ping(self): + self.receive_frame(Frame(True, OP_PING, b"test")) + self.run_loop_once() + self.assertOneFrameSent(True, OP_PONG, b"test") + + def test_answer_ping_does_not_crash_if_connection_closing(self): + close_task = self.half_close_connection_local() + + self.receive_frame(Frame(True, OP_PING, b"test")) + self.run_loop_once() + + with self.assertNoLogs(): + self.loop.run_until_complete(self.protocol.close()) + + self.loop.run_until_complete(close_task) # cleanup + + def test_answer_ping_does_not_crash_if_connection_closed(self): + self.make_drain_slow() + # Drop the connection right after receiving a ping frame, + # which prevents responding with a pong frame properly. + self.receive_frame(Frame(True, OP_PING, b"test")) + self.receive_eof() + self.run_loop_once() + + with self.assertNoLogs(): + self.loop.run_until_complete(self.protocol.close()) + + def test_ignore_pong(self): + self.receive_frame(Frame(True, OP_PONG, b"test")) + self.run_loop_once() + self.assertNoFrameSent() + + def test_acknowledge_ping(self): + pong_waiter = self.loop.run_until_complete(self.protocol.ping()) + self.assertFalse(pong_waiter.done()) + ping_frame = self.last_sent_frame() + pong_frame = Frame(True, OP_PONG, ping_frame.data) + self.receive_frame(pong_frame) + self.run_loop_once() + self.run_loop_once() + self.assertTrue(pong_waiter.done()) + + def test_abort_ping(self): + pong_waiter = self.loop.run_until_complete(self.protocol.ping()) + # Remove the frame from the buffer, else close_connection() complains. + self.last_sent_frame() + self.assertFalse(pong_waiter.done()) + self.close_connection() + self.assertTrue(pong_waiter.done()) + self.assertIsInstance(pong_waiter.exception(), ConnectionClosed) + + def test_abort_ping_does_not_log_exception_if_not_retreived(self): + self.loop.run_until_complete(self.protocol.ping()) + # Get the internal Future, which isn't directly returned by ping(). + ((pong_waiter, _timestamp),) = self.protocol.pings.values() + # Remove the frame from the buffer, else close_connection() complains. + self.last_sent_frame() + self.close_connection() + # Check a private attribute, for lack of a better solution. + self.assertFalse(pong_waiter._log_traceback) + + def test_acknowledge_previous_pings(self): + pings = [ + (self.loop.run_until_complete(self.protocol.ping()), self.last_sent_frame()) + for i in range(3) + ] + # Unsolicited pong doesn't acknowledge pings + self.receive_frame(Frame(True, OP_PONG, b"")) + self.run_loop_once() + self.run_loop_once() + self.assertFalse(pings[0][0].done()) + self.assertFalse(pings[1][0].done()) + self.assertFalse(pings[2][0].done()) + # Pong acknowledges all previous pings + self.receive_frame(Frame(True, OP_PONG, pings[1][1].data)) + self.run_loop_once() + self.run_loop_once() + self.assertTrue(pings[0][0].done()) + self.assertTrue(pings[1][0].done()) + self.assertFalse(pings[2][0].done()) + + def test_acknowledge_aborted_ping(self): + pong_waiter = self.loop.run_until_complete(self.protocol.ping()) + ping_frame = self.last_sent_frame() + # Clog incoming queue. This lets connection_lost() abort pending pings + # with a ConnectionClosed exception before transfer_data_task + # terminates and close_connection cancels keepalive_ping_task. + self.protocol.max_queue = 1 + self.receive_frame(Frame(True, OP_TEXT, b"1")) + self.receive_frame(Frame(True, OP_TEXT, b"2")) + # Add pong frame to the queue. + pong_frame = Frame(True, OP_PONG, ping_frame.data) + self.receive_frame(pong_frame) + # Connection drops. + self.receive_eof() + self.loop.run_until_complete(self.protocol.wait_closed()) + # Ping receives a ConnectionClosed exception. + with self.assertRaises(ConnectionClosed): + pong_waiter.result() + + # transfer_data doesn't crash, which would be logged. + with self.assertNoLogs(): + # Unclog incoming queue. + self.loop.run_until_complete(self.protocol.recv()) + self.loop.run_until_complete(self.protocol.recv()) + + def test_canceled_ping(self): + pong_waiter = self.loop.run_until_complete(self.protocol.ping()) + ping_frame = self.last_sent_frame() + pong_waiter.cancel() + pong_frame = Frame(True, OP_PONG, ping_frame.data) + self.receive_frame(pong_frame) + self.run_loop_once() + self.run_loop_once() + self.assertTrue(pong_waiter.cancelled()) + + def test_duplicate_ping(self): + self.loop.run_until_complete(self.protocol.ping(b"foobar")) + self.assertOneFrameSent(True, OP_PING, b"foobar") + with self.assertRaises(RuntimeError): + self.loop.run_until_complete(self.protocol.ping(b"foobar")) + self.assertNoFrameSent() + + # Test the protocol's logic for measuring latency + + def test_record_latency_on_pong(self): + self.assertEqual(self.protocol.latency, 0) + self.loop.run_until_complete(self.protocol.ping(b"test")) + self.receive_frame(Frame(True, OP_PONG, b"test")) + self.run_loop_once() + self.assertGreater(self.protocol.latency, 0) + + def test_return_latency_on_pong(self): + pong_waiter = self.loop.run_until_complete(self.protocol.ping()) + ping_frame = self.last_sent_frame() + pong_frame = Frame(True, OP_PONG, ping_frame.data) + self.receive_frame(pong_frame) + latency = self.loop.run_until_complete(pong_waiter) + self.assertGreater(latency, 0) + + # Test the protocol's logic for rebuilding fragmented messages. + + def test_fragmented_text(self): + self.receive_frame(Frame(False, OP_TEXT, "ca".encode("utf-8"))) + self.receive_frame(Frame(True, OP_CONT, "fé".encode("utf-8"))) + data = self.loop.run_until_complete(self.protocol.recv()) + self.assertEqual(data, "café") + + def test_fragmented_binary(self): + self.receive_frame(Frame(False, OP_BINARY, b"t")) + self.receive_frame(Frame(False, OP_CONT, b"e")) + self.receive_frame(Frame(True, OP_CONT, b"a")) + data = self.loop.run_until_complete(self.protocol.recv()) + self.assertEqual(data, b"tea") + + def test_fragmented_text_payload_too_big(self): + self.protocol.max_size = 1024 + self.receive_frame(Frame(False, OP_TEXT, "café".encode("utf-8") * 100)) + self.receive_frame(Frame(True, OP_CONT, "café".encode("utf-8") * 105)) + self.process_invalid_frames() + self.assertConnectionFailed(CloseCode.MESSAGE_TOO_BIG, "") + + def test_fragmented_binary_payload_too_big(self): + self.protocol.max_size = 1024 + self.receive_frame(Frame(False, OP_BINARY, b"tea" * 171)) + self.receive_frame(Frame(True, OP_CONT, b"tea" * 171)) + self.process_invalid_frames() + self.assertConnectionFailed(CloseCode.MESSAGE_TOO_BIG, "") + + def test_fragmented_text_no_max_size(self): + self.protocol.max_size = None # for test coverage + self.receive_frame(Frame(False, OP_TEXT, "café".encode("utf-8") * 100)) + self.receive_frame(Frame(True, OP_CONT, "café".encode("utf-8") * 105)) + data = self.loop.run_until_complete(self.protocol.recv()) + self.assertEqual(data, "café" * 205) + + def test_fragmented_binary_no_max_size(self): + self.protocol.max_size = None # for test coverage + self.receive_frame(Frame(False, OP_BINARY, b"tea" * 171)) + self.receive_frame(Frame(True, OP_CONT, b"tea" * 171)) + data = self.loop.run_until_complete(self.protocol.recv()) + self.assertEqual(data, b"tea" * 342) + + def test_control_frame_within_fragmented_text(self): + self.receive_frame(Frame(False, OP_TEXT, "ca".encode("utf-8"))) + self.receive_frame(Frame(True, OP_PING, b"")) + self.receive_frame(Frame(True, OP_CONT, "fé".encode("utf-8"))) + data = self.loop.run_until_complete(self.protocol.recv()) + self.assertEqual(data, "café") + self.assertOneFrameSent(True, OP_PONG, b"") + + def test_unterminated_fragmented_text(self): + self.receive_frame(Frame(False, OP_TEXT, "ca".encode("utf-8"))) + # Missing the second part of the fragmented frame. + self.receive_frame(Frame(True, OP_BINARY, b"tea")) + self.process_invalid_frames() + self.assertConnectionFailed(CloseCode.PROTOCOL_ERROR, "") + + def test_close_handshake_in_fragmented_text(self): + self.receive_frame(Frame(False, OP_TEXT, "ca".encode("utf-8"))) + self.receive_frame(Frame(True, OP_CLOSE, b"")) + self.process_invalid_frames() + # The RFC may have overlooked this case: it says that control frames + # can be interjected in the middle of a fragmented message and that a + # close frame must be echoed. Even though there's an unterminated + # message, technically, the closing handshake was successful. + self.assertConnectionClosed(CloseCode.NO_STATUS_RCVD, "") + + def test_connection_close_in_fragmented_text(self): + self.receive_frame(Frame(False, OP_TEXT, "ca".encode("utf-8"))) + self.process_invalid_frames() + self.assertConnectionFailed(CloseCode.ABNORMAL_CLOSURE, "") + + # Test miscellaneous code paths to ensure full coverage. + + def test_connection_lost(self): + # Test calling connection_lost without going through close_connection. + self.protocol.connection_lost(None) + + self.assertConnectionFailed(CloseCode.ABNORMAL_CLOSURE, "") + + def test_ensure_open_before_opening_handshake(self): + # Simulate a bug by forcibly reverting the protocol state. + self.protocol.state = State.CONNECTING + + with self.assertRaises(InvalidState): + self.loop.run_until_complete(self.protocol.ensure_open()) + + def test_ensure_open_during_unclean_close(self): + # Process connection_made in order to start transfer_data_task. + self.run_loop_once() + + # Ensure the test terminates quickly. + self.loop.call_later(MS, self.receive_eof_if_client) + + # Simulate the case when close() times out sending a close frame. + self.protocol.fail_connection() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.ensure_open()) + + def test_legacy_recv(self): + # By default legacy_recv in disabled. + self.assertEqual(self.protocol.legacy_recv, False) + + self.close_connection() + + # Enable legacy_recv. + self.protocol.legacy_recv = True + + # Now recv() returns None instead of raising ConnectionClosed. + self.assertIsNone(self.loop.run_until_complete(self.protocol.recv())) + + def test_connection_closed_attributes(self): + self.close_connection() + + with self.assertRaises(ConnectionClosed) as context: + self.loop.run_until_complete(self.protocol.recv()) + + connection_closed_exc = context.exception + self.assertEqual(connection_closed_exc.code, CloseCode.NORMAL_CLOSURE) + self.assertEqual(connection_closed_exc.reason, "close") + + # Test the protocol logic for sending keepalive pings. + + def restart_protocol_with_keepalive_ping( + self, + ping_interval=3 * MS, + ping_timeout=3 * MS, + ): + initial_protocol = self.protocol + + # copied from tearDown + + self.transport.close() + self.loop.run_until_complete(self.protocol.close()) + + # copied from setUp, but enables keepalive pings + + async def create_protocol(): + return WebSocketCommonProtocol( + ping_interval=ping_interval, + ping_timeout=ping_timeout, + ) + + self.protocol = self.loop.run_until_complete(create_protocol()) + + self.transport = TransportMock() + self.transport.setup_mock(self.loop, self.protocol) + self.protocol.is_client = initial_protocol.is_client + self.protocol.side = initial_protocol.side + + def test_keepalive_ping(self): + self.restart_protocol_with_keepalive_ping() + + # Ping is sent at 3ms and acknowledged at 4ms. + self.loop.run_until_complete(asyncio.sleep(4 * MS)) + (ping_1,) = tuple(self.protocol.pings) + self.assertOneFrameSent(True, OP_PING, ping_1) + self.receive_frame(Frame(True, OP_PONG, ping_1)) + + # Next ping is sent at 7ms. + self.loop.run_until_complete(asyncio.sleep(4 * MS)) + (ping_2,) = tuple(self.protocol.pings) + self.assertOneFrameSent(True, OP_PING, ping_2) + + # The keepalive ping task goes on. + self.assertFalse(self.protocol.keepalive_ping_task.done()) + + def test_keepalive_ping_not_acknowledged_closes_connection(self): + self.restart_protocol_with_keepalive_ping() + + # Ping is sent at 3ms and not acknowledged. + self.loop.run_until_complete(asyncio.sleep(4 * MS)) + (ping_1,) = tuple(self.protocol.pings) + self.assertOneFrameSent(True, OP_PING, ping_1) + + # Connection is closed at 6ms. + self.loop.run_until_complete(asyncio.sleep(4 * MS)) + self.assertOneFrameSent( + True, + OP_CLOSE, + Close(CloseCode.INTERNAL_ERROR, "keepalive ping timeout").serialize(), + ) + + # The keepalive ping task is complete. + self.assertEqual(self.protocol.keepalive_ping_task.result(), None) + + def test_keepalive_ping_stops_when_connection_closing(self): + self.restart_protocol_with_keepalive_ping() + close_task = self.half_close_connection_local() + + # No ping sent at 3ms because the closing handshake is in progress. + self.loop.run_until_complete(asyncio.sleep(4 * MS)) + self.assertNoFrameSent() + + # The keepalive ping task terminated. + self.assertTrue(self.protocol.keepalive_ping_task.cancelled()) + + self.loop.run_until_complete(close_task) # cleanup + + def test_keepalive_ping_stops_when_connection_closed(self): + self.restart_protocol_with_keepalive_ping() + self.close_connection() + + # The keepalive ping task terminated. + self.assertTrue(self.protocol.keepalive_ping_task.cancelled()) + + def test_keepalive_ping_does_not_crash_when_connection_lost(self): + self.restart_protocol_with_keepalive_ping() + # Clog incoming queue. This lets connection_lost() abort pending pings + # with a ConnectionClosed exception before transfer_data_task + # terminates and close_connection cancels keepalive_ping_task. + self.protocol.max_queue = 1 + self.receive_frame(Frame(True, OP_TEXT, b"1")) + self.receive_frame(Frame(True, OP_TEXT, b"2")) + # Ping is sent at 3ms. + self.loop.run_until_complete(asyncio.sleep(4 * MS)) + ((pong_waiter, _timestamp),) = self.protocol.pings.values() + # Connection drops. + self.receive_eof() + self.loop.run_until_complete(self.protocol.wait_closed()) + + # The ping waiter receives a ConnectionClosed exception. + with self.assertRaises(ConnectionClosed): + pong_waiter.result() + # The keepalive ping task terminated properly. + self.assertIsNone(self.protocol.keepalive_ping_task.result()) + + # Unclog incoming queue to terminate the test quickly. + self.loop.run_until_complete(self.protocol.recv()) + self.loop.run_until_complete(self.protocol.recv()) + + def test_keepalive_ping_with_no_ping_interval(self): + self.restart_protocol_with_keepalive_ping(ping_interval=None) + + # No ping is sent at 3ms. + self.loop.run_until_complete(asyncio.sleep(4 * MS)) + self.assertNoFrameSent() + + def test_keepalive_ping_with_no_ping_timeout(self): + self.restart_protocol_with_keepalive_ping(ping_timeout=None) + + # Ping is sent at 3ms and not acknowledged. + self.loop.run_until_complete(asyncio.sleep(4 * MS)) + (ping_1,) = tuple(self.protocol.pings) + self.assertOneFrameSent(True, OP_PING, ping_1) + + # Next ping is sent at 7ms anyway. + self.loop.run_until_complete(asyncio.sleep(4 * MS)) + ping_1_again, ping_2 = tuple(self.protocol.pings) + self.assertEqual(ping_1, ping_1_again) + self.assertOneFrameSent(True, OP_PING, ping_2) + + # The keepalive ping task goes on. + self.assertFalse(self.protocol.keepalive_ping_task.done()) + + def test_keepalive_ping_unexpected_error(self): + self.restart_protocol_with_keepalive_ping() + + async def ping(): + raise Exception("BOOM") + + self.protocol.ping = ping + + # The keepalive ping task fails when sending a ping at 3ms. + self.loop.run_until_complete(asyncio.sleep(4 * MS)) + + # The keepalive ping task is complete. + # It logs and swallows the exception. + self.assertEqual(self.protocol.keepalive_ping_task.result(), None) + + # Test the protocol logic for closing the connection. + + def test_local_close(self): + # Emulate how the remote endpoint answers the closing handshake. + self.loop.call_later(MS, self.receive_frame, self.close_frame) + self.loop.call_later(MS, self.receive_eof_if_client) + + # Run the closing handshake. + self.loop.run_until_complete(self.protocol.close(reason="close")) + + self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "close") + self.assertOneFrameSent(*self.close_frame) + + # Closing the connection again is a no-op. + self.loop.run_until_complete(self.protocol.close(reason="oh noes!")) + + self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "close") + self.assertNoFrameSent() + + def test_remote_close(self): + # Emulate how the remote endpoint initiates the closing handshake. + self.loop.call_later(MS, self.receive_frame, self.close_frame) + self.loop.call_later(MS, self.receive_eof_if_client) + + # Wait for some data in order to process the handshake. + # After recv() raises ConnectionClosed, the connection is closed. + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(self.protocol.recv()) + + self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "close") + self.assertOneFrameSent(*self.close_frame) + + # Closing the connection again is a no-op. + self.loop.run_until_complete(self.protocol.close(reason="oh noes!")) + + self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "close") + self.assertNoFrameSent() + + def test_remote_close_and_connection_lost(self): + self.make_drain_slow() + # Drop the connection right after receiving a close frame, + # which prevents echoing the close frame properly. + self.receive_frame(self.close_frame) + self.receive_eof() + self.run_loop_once() + + with self.assertNoLogs(): + self.loop.run_until_complete(self.protocol.close(reason="oh noes!")) + + self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "close") + self.assertOneFrameSent(*self.close_frame) + + def test_simultaneous_close(self): + # Receive the incoming close frame right after self.protocol.close() + # starts executing. This reproduces the error described in: + # https://github.com/python-websockets/websockets/issues/339 + self.loop.call_soon(self.receive_frame, self.remote_close) + self.loop.call_soon(self.receive_eof_if_client) + self.run_loop_once() + + self.loop.run_until_complete(self.protocol.close(reason="local")) + + self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "remote") + # The current implementation sends a close frame in response to the + # close frame received from the remote end. It skips the close frame + # that should be sent as a result of calling close(). + self.assertOneFrameSent(*self.remote_close) + + def test_close_preserves_incoming_frames(self): + self.receive_frame(Frame(True, OP_TEXT, b"hello")) + self.run_loop_once() + + self.loop.call_later(MS, self.receive_frame, self.close_frame) + self.loop.call_later(MS, self.receive_eof_if_client) + self.loop.run_until_complete(self.protocol.close(reason="close")) + + self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "close") + self.assertOneFrameSent(*self.close_frame) + + next_message = self.loop.run_until_complete(self.protocol.recv()) + self.assertEqual(next_message, "hello") + + def test_close_protocol_error(self): + invalid_close_frame = Frame(True, OP_CLOSE, b"\x00") + self.receive_frame(invalid_close_frame) + self.receive_eof_if_client() + self.run_loop_once() + self.loop.run_until_complete(self.protocol.close(reason="close")) + + self.assertConnectionFailed(CloseCode.PROTOCOL_ERROR, "") + + def test_close_connection_lost(self): + self.receive_eof() + self.run_loop_once() + self.loop.run_until_complete(self.protocol.close(reason="close")) + + self.assertConnectionFailed(CloseCode.ABNORMAL_CLOSURE, "") + + def test_local_close_during_recv(self): + recv = self.loop.create_task(self.protocol.recv()) + + self.loop.call_later(MS, self.receive_frame, self.close_frame) + self.loop.call_later(MS, self.receive_eof_if_client) + + self.loop.run_until_complete(self.protocol.close(reason="close")) + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(recv) + + self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "close") + + # There is no test_remote_close_during_recv because it would be identical + # to test_remote_close. + + def test_remote_close_during_send(self): + self.make_drain_slow() + send = self.loop.create_task(self.protocol.send("hello")) + + self.receive_frame(self.close_frame) + self.receive_eof() + + with self.assertRaises(ConnectionClosed): + self.loop.run_until_complete(send) + + self.assertConnectionClosed(CloseCode.NORMAL_CLOSURE, "close") + + # There is no test_local_close_during_send because this cannot really + # happen, considering that writes are serialized. + + def test_broadcast_text(self): + broadcast([self.protocol], "café") + self.assertOneFrameSent(True, OP_TEXT, "café".encode("utf-8")) + + def test_broadcast_binary(self): + broadcast([self.protocol], b"tea") + self.assertOneFrameSent(True, OP_BINARY, b"tea") + + def test_broadcast_type_error(self): + with self.assertRaises(TypeError): + broadcast([self.protocol], ["ca", "fé"]) + + def test_broadcast_no_clients(self): + broadcast([], "café") + self.assertNoFrameSent() + + def test_broadcast_two_clients(self): + broadcast([self.protocol, self.protocol], "café") + self.assertFramesSent( + (True, OP_TEXT, "café".encode("utf-8")), + (True, OP_TEXT, "café".encode("utf-8")), + ) + + def test_broadcast_skips_closed_connection(self): + self.close_connection() + + with self.assertNoLogs(): + broadcast([self.protocol], "café") + self.assertNoFrameSent() + + def test_broadcast_skips_closing_connection(self): + close_task = self.half_close_connection_local() + + with self.assertNoLogs(): + broadcast([self.protocol], "café") + self.assertNoFrameSent() + + self.loop.run_until_complete(close_task) # cleanup + + def test_broadcast_skips_connection_sending_fragmented_text(self): + self.make_drain_slow() + self.loop.create_task(self.protocol.send(["ca", "fé"])) + self.run_loop_once() + self.assertOneFrameSent(False, OP_TEXT, "ca".encode("utf-8")) + + with self.assertLogs("websockets", logging.WARNING) as logs: + broadcast([self.protocol], "café") + + self.assertEqual( + [record.getMessage() for record in logs.records][:2], + ["skipped broadcast: sending a fragmented message"], + ) + + @unittest.skipIf( + sys.version_info[:2] < (3, 11), "raise_exceptions requires Python 3.11+" + ) + def test_broadcast_reports_connection_sending_fragmented_text(self): + self.make_drain_slow() + self.loop.create_task(self.protocol.send(["ca", "fé"])) + self.run_loop_once() + self.assertOneFrameSent(False, OP_TEXT, "ca".encode("utf-8")) + + with self.assertRaises(ExceptionGroup) as raised: + broadcast([self.protocol], "café", raise_exceptions=True) + + self.assertEqual(str(raised.exception), "skipped broadcast (1 sub-exception)") + self.assertEqual( + str(raised.exception.exceptions[0]), "sending a fragmented message" + ) + + def test_broadcast_skips_connection_failing_to_send(self): + # Configure mock to raise an exception when writing to the network. + self.protocol.transport.write.side_effect = RuntimeError + + with self.assertLogs("websockets", logging.WARNING) as logs: + broadcast([self.protocol], "café") + + self.assertEqual( + [record.getMessage() for record in logs.records][:2], + ["skipped broadcast: failed to write message"], + ) + + @unittest.skipIf( + sys.version_info[:2] < (3, 11), "raise_exceptions requires Python 3.11+" + ) + def test_broadcast_reports_connection_failing_to_send(self): + # Configure mock to raise an exception when writing to the network. + self.protocol.transport.write.side_effect = RuntimeError("BOOM") + + with self.assertRaises(ExceptionGroup) as raised: + broadcast([self.protocol], "café", raise_exceptions=True) + + self.assertEqual(str(raised.exception), "skipped broadcast (1 sub-exception)") + self.assertEqual(str(raised.exception.exceptions[0]), "failed to write message") + self.assertEqual(str(raised.exception.exceptions[0].__cause__), "BOOM") + + +class ServerTests(CommonTests, AsyncioTestCase): + def setUp(self): + super().setUp() + self.protocol.is_client = False + self.protocol.side = "server" + + def test_local_close_send_close_frame_timeout(self): + self.protocol.close_timeout = 10 * MS + self.make_drain_slow(50 * MS) + # If we can't send a close frame, time out in 10ms. + # Check the timing within -1/+9ms for robustness. + with self.assertCompletesWithin(9 * MS, 19 * MS): + self.loop.run_until_complete(self.protocol.close(reason="close")) + self.assertConnectionClosed(CloseCode.ABNORMAL_CLOSURE, "") + + def test_local_close_receive_close_frame_timeout(self): + self.protocol.close_timeout = 10 * MS + # If the client doesn't send a close frame, time out in 10ms. + # Check the timing within -1/+9ms for robustness. + with self.assertCompletesWithin(9 * MS, 19 * MS): + self.loop.run_until_complete(self.protocol.close(reason="close")) + self.assertConnectionClosed(CloseCode.ABNORMAL_CLOSURE, "") + + def test_local_close_connection_lost_timeout_after_write_eof(self): + self.protocol.close_timeout = 10 * MS + # If the client doesn't close its side of the TCP connection after we + # half-close our side with write_eof(), time out in 10ms. + # Check the timing within -1/+9ms for robustness. + with self.assertCompletesWithin(9 * MS, 19 * MS): + # HACK: disable write_eof => other end drops connection emulation. + self.transport._eof = True + self.receive_frame(self.close_frame) + self.run_loop_once() + self.loop.run_until_complete(self.protocol.close(reason="close")) + # Due to a bug in coverage, this is erroneously reported as not covered. + self.assertConnectionClosed( # pragma: no cover + CloseCode.NORMAL_CLOSURE, + "close", + ) + + def test_local_close_connection_lost_timeout_after_close(self): + self.protocol.close_timeout = 10 * MS + # If the client doesn't close its side of the TCP connection after we + # half-close our side with write_eof() and close it with close(), time + # out in 20ms. + # Check the timing within -1/+9ms for robustness. + # Add another 10ms because this test is flaky and I don't understand. + with self.assertCompletesWithin(19 * MS, 39 * MS): + # HACK: disable write_eof => other end drops connection emulation. + self.transport._eof = True + # HACK: disable close => other end drops connection emulation. + self.transport._closing = True + self.receive_frame(self.close_frame) + self.run_loop_once() + self.loop.run_until_complete(self.protocol.close(reason="close")) + # Due to a bug in coverage, this is erroneously reported as not covered. + self.assertConnectionClosed( # pragma: no cover + CloseCode.NORMAL_CLOSURE, + "close", + ) + + +class ClientTests(CommonTests, AsyncioTestCase): + def setUp(self): + super().setUp() + self.protocol.is_client = True + self.protocol.side = "client" + + def test_local_close_send_close_frame_timeout(self): + self.protocol.close_timeout = 10 * MS + self.make_drain_slow(50 * MS) + # If we can't send a close frame, time out in 20ms. + # - 10ms waiting for sending a close frame + # - 10ms waiting for receiving a half-close + # Check the timing within -1/+9ms for robustness. + with self.assertCompletesWithin(19 * MS, 29 * MS): + self.loop.run_until_complete(self.protocol.close(reason="close")) + # Due to a bug in coverage, this is erroneously reported as not covered. + self.assertConnectionClosed( # pragma: no cover + CloseCode.ABNORMAL_CLOSURE, + "", + ) + + def test_local_close_receive_close_frame_timeout(self): + self.protocol.close_timeout = 10 * MS + # If the server doesn't send a close frame, time out in 20ms: + # - 10ms waiting for receiving a close frame + # - 10ms waiting for receiving a half-close + # Check the timing within -1/+9ms for robustness. + with self.assertCompletesWithin(19 * MS, 29 * MS): + self.loop.run_until_complete(self.protocol.close(reason="close")) + # Due to a bug in coverage, this is erroneously reported as not covered. + self.assertConnectionClosed( # pragma: no cover + CloseCode.ABNORMAL_CLOSURE, + "", + ) + + def test_local_close_connection_lost_timeout_after_write_eof(self): + self.protocol.close_timeout = 10 * MS + # If the server doesn't half-close its side of the TCP connection + # after we send a close frame, time out in 20ms: + # - 10ms waiting for receiving a half-close + # - 10ms waiting for receiving a close after write_eof + # Check the timing within -1/+9ms for robustness. + with self.assertCompletesWithin(19 * MS, 29 * MS): + # HACK: disable write_eof => other end drops connection emulation. + self.transport._eof = True + self.receive_frame(self.close_frame) + self.run_loop_once() + self.loop.run_until_complete(self.protocol.close(reason="close")) + # Due to a bug in coverage, this is erroneously reported as not covered. + self.assertConnectionClosed( # pragma: no cover + CloseCode.NORMAL_CLOSURE, + "close", + ) + + def test_local_close_connection_lost_timeout_after_close(self): + self.protocol.close_timeout = 10 * MS + # If the client doesn't close its side of the TCP connection after we + # half-close our side with write_eof() and close it with close(), time + # out in 30ms. + # - 10ms waiting for receiving a half-close + # - 10ms waiting for receiving a close after write_eof + # - 10ms waiting for receiving a close after close + # Check the timing within -1/+9ms for robustness. + # Add another 10ms because this test is flaky and I don't understand. + with self.assertCompletesWithin(29 * MS, 49 * MS): + # HACK: disable write_eof => other end drops connection emulation. + self.transport._eof = True + # HACK: disable close => other end drops connection emulation. + self.transport._closing = True + self.receive_frame(self.close_frame) + self.run_loop_once() + self.loop.run_until_complete(self.protocol.close(reason="close")) + # Due to a bug in coverage, this is erroneously reported as not covered. + self.assertConnectionClosed( # pragma: no cover + CloseCode.NORMAL_CLOSURE, + "close", + ) diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/legacy/utils.py b/tests/wpt/tests/tools/third_party/websockets/tests/legacy/utils.py new file mode 100644 index 00000000000..4a21dcaeb59 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/legacy/utils.py @@ -0,0 +1,84 @@ +import asyncio +import contextlib +import functools +import logging +import unittest + + +class AsyncioTestCase(unittest.TestCase): + """ + Base class for tests that sets up an isolated event loop for each test. + + IsolatedAsyncioTestCase was introduced in Python 3.8 for similar purposes + but isn't a drop-in replacement. + + """ + + def __init_subclass__(cls, **kwargs): + """ + Convert test coroutines to test functions. + + This supports asynchronous tests transparently. + + """ + super().__init_subclass__(**kwargs) + for name in unittest.defaultTestLoader.getTestCaseNames(cls): + test = getattr(cls, name) + if asyncio.iscoroutinefunction(test): + setattr(cls, name, cls.convert_async_to_sync(test)) + + @staticmethod + def convert_async_to_sync(test): + """ + Convert a test coroutine to a test function. + + """ + + @functools.wraps(test) + def test_func(self, *args, **kwargs): + return self.loop.run_until_complete(test(self, *args, **kwargs)) + + return test_func + + def setUp(self): + super().setUp() + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + + def tearDown(self): + self.loop.close() + super().tearDown() + + def run_loop_once(self): + # Process callbacks scheduled with call_soon by appending a callback + # to stop the event loop then running it until it hits that callback. + self.loop.call_soon(self.loop.stop) + self.loop.run_forever() + + # Remove when dropping Python < 3.10 + @contextlib.contextmanager + def assertNoLogs(self, logger="websockets", level=logging.ERROR): + """ + No message is logged on the given logger with at least the given level. + + """ + with self.assertLogs(logger, level) as logs: + # We want to test that no log message is emitted + # but assertLogs expects at least one log message. + logging.getLogger(logger).log(level, "dummy") + yield + + level_name = logging.getLevelName(level) + self.assertEqual(logs.output, [f"{level_name}:{logger}:dummy"]) + + def assertDeprecationWarnings(self, recorded_warnings, expected_warnings): + """ + Check recorded deprecation warnings match a list of expected messages. + + """ + for recorded in recorded_warnings: + self.assertEqual(type(recorded.message), DeprecationWarning) + self.assertEqual( + set(str(recorded.message) for recorded in recorded_warnings), + set(expected_warnings), + ) diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/maxi_cov.py b/tests/wpt/tests/tools/third_party/websockets/tests/maxi_cov.py new file mode 100755 index 00000000000..2568dcf18bc --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/maxi_cov.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python + +"""Measure coverage of each module by its test module.""" + +import glob +import os.path +import subprocess +import sys + + +UNMAPPED_SRC_FILES = ["websockets/version.py"] +UNMAPPED_TEST_FILES = ["tests/test_exports.py"] + + +def check_environment(): + """Check that prerequisites for running this script are met.""" + try: + import websockets # noqa: F401 + except ImportError: + print("failed to import websockets; is src on PYTHONPATH?") + return False + try: + import coverage # noqa: F401 + except ImportError: + print("failed to locate Coverage.py; is it installed?") + return False + return True + + +def get_mapping(src_dir="src"): + """Return a dict mapping each source file to its test file.""" + + # List source and test files. + + src_files = glob.glob( + os.path.join(src_dir, "websockets/**/*.py"), + recursive=True, + ) + test_files = glob.glob( + "tests/**/*.py", + recursive=True, + ) + + src_files = [ + os.path.relpath(src_file, src_dir) + for src_file in sorted(src_files) + if "legacy" not in os.path.dirname(src_file) + if os.path.basename(src_file) != "__init__.py" + and os.path.basename(src_file) != "__main__.py" + and os.path.basename(src_file) != "compatibility.py" + ] + test_files = [ + test_file + for test_file in sorted(test_files) + if "legacy" not in os.path.dirname(test_file) + and os.path.basename(test_file) != "__init__.py" + and os.path.basename(test_file).startswith("test_") + ] + + # Map source files to test files. + + mapping = {} + unmapped_test_files = [] + + for test_file in test_files: + dir_name, file_name = os.path.split(test_file) + assert dir_name.startswith("tests") + assert file_name.startswith("test_") + src_file = os.path.join( + "websockets" + dir_name[len("tests") :], + file_name[len("test_") :], + ) + if src_file in src_files: + mapping[src_file] = test_file + else: + unmapped_test_files.append(test_file) + + unmapped_src_files = list(set(src_files) - set(mapping)) + + # Ensure that all files are mapped. + + assert unmapped_src_files == UNMAPPED_SRC_FILES + assert unmapped_test_files == UNMAPPED_TEST_FILES + + return mapping + + +def get_ignored_files(src_dir="src"): + """Return the list of files to exclude from coverage measurement.""" + + return [ + # */websockets matches src/websockets and .tox/**/site-packages/websockets. + # There are no tests for the __main__ module and for compatibility modules. + "*/websockets/__main__.py", + "*/websockets/*/compatibility.py", + # This approach isn't applicable to the test suite of the legacy + # implementation, due to the huge test_client_server test module. + "*/websockets/legacy/*", + "tests/legacy/*", + ] + [ + # Exclude test utilities that are shared between several test modules. + # Also excludes this script. + test_file + for test_file in sorted(glob.glob("tests/**/*.py", recursive=True)) + if "legacy" not in os.path.dirname(test_file) + and os.path.basename(test_file) != "__init__.py" + and not os.path.basename(test_file).startswith("test_") + ] + + +def run_coverage(mapping, src_dir="src"): + # Initialize a new coverage measurement session. The --source option + # includes all files in the report, even if they're never imported. + print("\nInitializing session\n", flush=True) + subprocess.run( + [ + sys.executable, + "-m", + "coverage", + "run", + "--source", + ",".join([os.path.join(src_dir, "websockets"), "tests"]), + "--omit", + ",".join(get_ignored_files(src_dir)), + "-m", + "unittest", + ] + + UNMAPPED_TEST_FILES, + check=True, + ) + # Append coverage of each source module by the corresponding test module. + for src_file, test_file in mapping.items(): + print(f"\nTesting {src_file} with {test_file}\n", flush=True) + subprocess.run( + [ + sys.executable, + "-m", + "coverage", + "run", + "--append", + "--include", + ",".join([os.path.join(src_dir, src_file), test_file]), + "-m", + "unittest", + test_file, + ], + check=True, + ) + + +if __name__ == "__main__": + if not check_environment(): + sys.exit(1) + src_dir = sys.argv[1] if len(sys.argv) == 2 else "src" + mapping = get_mapping(src_dir) + run_coverage(mapping, src_dir) diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/protocol.py b/tests/wpt/tests/tools/third_party/websockets/tests/protocol.py new file mode 100644 index 00000000000..4e843daab34 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/protocol.py @@ -0,0 +1,29 @@ +from websockets.protocol import Protocol + + +class RecordingProtocol(Protocol): + """ + Protocol subclass that records incoming frames. + + By interfacing with this protocol, you can check easily what the component + being testing sends during a test. + + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.frames_rcvd = [] + + def get_frames_rcvd(self): + """ + Get incoming frames received up to this point. + + Calling this method clears the list. Each frame is returned only once. + + """ + frames_rcvd, self.frames_rcvd = self.frames_rcvd, [] + return frames_rcvd + + def recv_frame(self, frame): + self.frames_rcvd.append(frame) + super().recv_frame(frame) diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/sync/__init__.py b/tests/wpt/tests/tools/third_party/websockets/tests/sync/__init__.py new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/sync/__init__.py diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/sync/client.py b/tests/wpt/tests/tools/third_party/websockets/tests/sync/client.py new file mode 100644 index 00000000000..683893e88c9 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/sync/client.py @@ -0,0 +1,39 @@ +import contextlib +import ssl + +from websockets.sync.client import * +from websockets.sync.server import WebSocketServer + +from ..utils import CERTIFICATE + + +__all__ = [ + "CLIENT_CONTEXT", + "run_client", + "run_unix_client", +] + + +CLIENT_CONTEXT = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) +CLIENT_CONTEXT.load_verify_locations(CERTIFICATE) + + +@contextlib.contextmanager +def run_client(wsuri_or_server, secure=None, resource_name="/", **kwargs): + if isinstance(wsuri_or_server, str): + wsuri = wsuri_or_server + else: + assert isinstance(wsuri_or_server, WebSocketServer) + if secure is None: + secure = "ssl_context" in kwargs + protocol = "wss" if secure else "ws" + host, port = wsuri_or_server.socket.getsockname() + wsuri = f"{protocol}://{host}:{port}{resource_name}" + with connect(wsuri, **kwargs) as client: + yield client + + +@contextlib.contextmanager +def run_unix_client(path, **kwargs): + with unix_connect(path, **kwargs) as client: + yield client diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/sync/connection.py b/tests/wpt/tests/tools/third_party/websockets/tests/sync/connection.py new file mode 100644 index 00000000000..89d4909ee13 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/sync/connection.py @@ -0,0 +1,109 @@ +import contextlib +import time + +from websockets.sync.connection import Connection + + +class InterceptingConnection(Connection): + """ + Connection subclass that can intercept outgoing packets. + + By interfacing with this connection, you can simulate network conditions + affecting what the component being tested receives during a test. + + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.socket = InterceptingSocket(self.socket) + + @contextlib.contextmanager + def delay_frames_sent(self, delay): + """ + Add a delay before sending frames. + + Delays cumulate: they're added before every frame or before EOF. + + """ + assert self.socket.delay_sendall is None + self.socket.delay_sendall = delay + try: + yield + finally: + self.socket.delay_sendall = None + + @contextlib.contextmanager + def delay_eof_sent(self, delay): + """ + Add a delay before sending EOF. + + Delays cumulate: they're added before every frame or before EOF. + + """ + assert self.socket.delay_shutdown is None + self.socket.delay_shutdown = delay + try: + yield + finally: + self.socket.delay_shutdown = None + + @contextlib.contextmanager + def drop_frames_sent(self): + """ + Prevent frames from being sent. + + Since TCP is reliable, sending frames or EOF afterwards is unrealistic. + + """ + assert not self.socket.drop_sendall + self.socket.drop_sendall = True + try: + yield + finally: + self.socket.drop_sendall = False + + @contextlib.contextmanager + def drop_eof_sent(self): + """ + Prevent EOF from being sent. + + Since TCP is reliable, sending frames or EOF afterwards is unrealistic. + + """ + assert not self.socket.drop_shutdown + self.socket.drop_shutdown = True + try: + yield + finally: + self.socket.drop_shutdown = False + + +class InterceptingSocket: + """ + Socket wrapper that intercepts calls to sendall and shutdown. + + This is coupled to the implementation, which relies on these two methods. + + """ + + def __init__(self, socket): + self.socket = socket + self.delay_sendall = None + self.delay_shutdown = None + self.drop_sendall = False + self.drop_shutdown = False + + def __getattr__(self, name): + return getattr(self.socket, name) + + def sendall(self, bytes, flags=0): + if self.delay_sendall is not None: + time.sleep(self.delay_sendall) + if not self.drop_sendall: + self.socket.sendall(bytes, flags) + + def shutdown(self, how): + if self.delay_shutdown is not None: + time.sleep(self.delay_shutdown) + if not self.drop_shutdown: + self.socket.shutdown(how) diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/sync/server.py b/tests/wpt/tests/tools/third_party/websockets/tests/sync/server.py new file mode 100644 index 00000000000..a9a77438ca9 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/sync/server.py @@ -0,0 +1,65 @@ +import contextlib +import ssl +import threading + +from websockets.sync.server import * + +from ..utils import CERTIFICATE + + +SERVER_CONTEXT = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) +SERVER_CONTEXT.load_cert_chain(CERTIFICATE) + +# Work around https://github.com/openssl/openssl/issues/7967 + +# This bug causes connect() to hang in tests for the client. Including this +# workaround acknowledges that the issue could happen outside of the test suite. + +# It shouldn't happen too often, or else OpenSSL 1.1.1 would be unusable. If it +# happens, we can look for a library-level fix, but it won't be easy. + +SERVER_CONTEXT.num_tickets = 0 + + +def crash(ws): + raise RuntimeError + + +def do_nothing(ws): + pass + + +def eval_shell(ws): + for expr in ws: + value = eval(expr) + ws.send(str(value)) + + +class EvalShellMixin: + def assertEval(self, client, expr, value): + client.send(expr) + self.assertEqual(client.recv(), value) + + +@contextlib.contextmanager +def run_server(ws_handler=eval_shell, host="localhost", port=0, **kwargs): + with serve(ws_handler, host, port, **kwargs) as server: + thread = threading.Thread(target=server.serve_forever) + thread.start() + try: + yield server + finally: + server.shutdown() + thread.join() + + +@contextlib.contextmanager +def run_unix_server(path, ws_handler=eval_shell, **kwargs): + with unix_serve(ws_handler, path, **kwargs) as server: + thread = threading.Thread(target=server.serve_forever) + thread.start() + try: + yield server + finally: + server.shutdown() + thread.join() diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/sync/test_client.py b/tests/wpt/tests/tools/third_party/websockets/tests/sync/test_client.py new file mode 100644 index 00000000000..c900f3b0fe6 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/sync/test_client.py @@ -0,0 +1,274 @@ +import socket +import ssl +import threading +import unittest + +from websockets.exceptions import InvalidHandshake +from websockets.extensions.permessage_deflate import PerMessageDeflate +from websockets.sync.client import * + +from ..utils import MS, temp_unix_socket_path +from .client import CLIENT_CONTEXT, run_client, run_unix_client +from .server import SERVER_CONTEXT, do_nothing, run_server, run_unix_server + + +class ClientTests(unittest.TestCase): + def test_connection(self): + """Client connects to server and the handshake succeeds.""" + with run_server() as server: + with run_client(server) as client: + self.assertEqual(client.protocol.state.name, "OPEN") + + def test_connection_fails(self): + """Client connects to server but the handshake fails.""" + + def remove_accept_header(self, request, response): + del response.headers["Sec-WebSocket-Accept"] + + # The connection will be open for the server but failed for the client. + # Use a connection handler that exits immediately to avoid an exception. + with run_server(do_nothing, process_response=remove_accept_header) as server: + with self.assertRaisesRegex( + InvalidHandshake, + "missing Sec-WebSocket-Accept header", + ): + with run_client(server, close_timeout=MS): + self.fail("did not raise") + + def test_tcp_connection_fails(self): + """Client fails to connect to server.""" + with self.assertRaises(OSError): + with run_client("ws://localhost:54321"): # invalid port + self.fail("did not raise") + + def test_existing_socket(self): + """Client connects using a pre-existing socket.""" + with run_server() as server: + with socket.create_connection(server.socket.getsockname()) as sock: + # Use a non-existing domain to ensure we connect to the right socket. + with run_client("ws://invalid/", sock=sock) as client: + self.assertEqual(client.protocol.state.name, "OPEN") + + def test_additional_headers(self): + """Client can set additional headers with additional_headers.""" + with run_server() as server: + with run_client( + server, additional_headers={"Authorization": "Bearer ..."} + ) as client: + self.assertEqual(client.request.headers["Authorization"], "Bearer ...") + + def test_override_user_agent(self): + """Client can override User-Agent header with user_agent_header.""" + with run_server() as server: + with run_client(server, user_agent_header="Smith") as client: + self.assertEqual(client.request.headers["User-Agent"], "Smith") + + def test_remove_user_agent(self): + """Client can remove User-Agent header with user_agent_header.""" + with run_server() as server: + with run_client(server, user_agent_header=None) as client: + self.assertNotIn("User-Agent", client.request.headers) + + def test_compression_is_enabled(self): + """Client enables compression by default.""" + with run_server() as server: + with run_client(server) as client: + self.assertEqual( + [type(ext) for ext in client.protocol.extensions], + [PerMessageDeflate], + ) + + def test_disable_compression(self): + """Client disables compression.""" + with run_server() as server: + with run_client(server, compression=None) as client: + self.assertEqual(client.protocol.extensions, []) + + def test_custom_connection_factory(self): + """Client runs ClientConnection factory provided in create_connection.""" + + def create_connection(*args, **kwargs): + client = ClientConnection(*args, **kwargs) + client.create_connection_ran = True + return client + + with run_server() as server: + with run_client(server, create_connection=create_connection) as client: + self.assertTrue(client.create_connection_ran) + + def test_timeout_during_handshake(self): + """Client times out before receiving handshake response from server.""" + gate = threading.Event() + + def stall_connection(self, request): + gate.wait() + + # The connection will be open for the server but failed for the client. + # Use a connection handler that exits immediately to avoid an exception. + with run_server(do_nothing, process_request=stall_connection) as server: + try: + with self.assertRaisesRegex( + TimeoutError, + "timed out during handshake", + ): + # While it shouldn't take 50ms to open a connection, this + # test becomes flaky in CI when setting a smaller timeout, + # even after increasing WEBSOCKETS_TESTS_TIMEOUT_FACTOR. + with run_client(server, open_timeout=5 * MS): + self.fail("did not raise") + finally: + gate.set() + + def test_connection_closed_during_handshake(self): + """Client reads EOF before receiving handshake response from server.""" + + def close_connection(self, request): + self.close_socket() + + with run_server(process_request=close_connection) as server: + with self.assertRaisesRegex( + ConnectionError, + "connection closed during handshake", + ): + with run_client(server): + self.fail("did not raise") + + +class SecureClientTests(unittest.TestCase): + def test_connection(self): + """Client connects to server securely.""" + with run_server(ssl_context=SERVER_CONTEXT) as server: + with run_client(server, ssl_context=CLIENT_CONTEXT) as client: + self.assertEqual(client.protocol.state.name, "OPEN") + self.assertEqual(client.socket.version()[:3], "TLS") + + def test_set_server_hostname_implicitly(self): + """Client sets server_hostname to the host in the WebSocket URI.""" + with temp_unix_socket_path() as path: + with run_unix_server(path, ssl_context=SERVER_CONTEXT): + with run_unix_client( + path, + ssl_context=CLIENT_CONTEXT, + uri="wss://overridden/", + ) as client: + self.assertEqual(client.socket.server_hostname, "overridden") + + def test_set_server_hostname_explicitly(self): + """Client sets server_hostname to the value provided in argument.""" + with temp_unix_socket_path() as path: + with run_unix_server(path, ssl_context=SERVER_CONTEXT): + with run_unix_client( + path, + ssl_context=CLIENT_CONTEXT, + server_hostname="overridden", + ) as client: + self.assertEqual(client.socket.server_hostname, "overridden") + + def test_reject_invalid_server_certificate(self): + """Client rejects certificate where server certificate isn't trusted.""" + with run_server(ssl_context=SERVER_CONTEXT) as server: + with self.assertRaisesRegex( + ssl.SSLCertVerificationError, + r"certificate verify failed: self[ -]signed certificate", + ): + # The test certificate isn't trusted system-wide. + with run_client(server, secure=True): + self.fail("did not raise") + + def test_reject_invalid_server_hostname(self): + """Client rejects certificate where server hostname doesn't match.""" + with run_server(ssl_context=SERVER_CONTEXT) as server: + with self.assertRaisesRegex( + ssl.SSLCertVerificationError, + r"certificate verify failed: Hostname mismatch", + ): + # This hostname isn't included in the test certificate. + with run_client( + server, ssl_context=CLIENT_CONTEXT, server_hostname="invalid" + ): + self.fail("did not raise") + + +@unittest.skipUnless(hasattr(socket, "AF_UNIX"), "this test requires Unix sockets") +class UnixClientTests(unittest.TestCase): + def test_connection(self): + """Client connects to server over a Unix socket.""" + with temp_unix_socket_path() as path: + with run_unix_server(path): + with run_unix_client(path) as client: + self.assertEqual(client.protocol.state.name, "OPEN") + + def test_set_host_header(self): + """Client sets the Host header to the host in the WebSocket URI.""" + # This is part of the documented behavior of unix_connect(). + with temp_unix_socket_path() as path: + with run_unix_server(path): + with run_unix_client(path, uri="ws://overridden/") as client: + self.assertEqual(client.request.headers["Host"], "overridden") + + +@unittest.skipUnless(hasattr(socket, "AF_UNIX"), "this test requires Unix sockets") +class SecureUnixClientTests(unittest.TestCase): + def test_connection(self): + """Client connects to server securely over a Unix socket.""" + with temp_unix_socket_path() as path: + with run_unix_server(path, ssl_context=SERVER_CONTEXT): + with run_unix_client(path, ssl_context=CLIENT_CONTEXT) as client: + self.assertEqual(client.protocol.state.name, "OPEN") + self.assertEqual(client.socket.version()[:3], "TLS") + + def test_set_server_hostname(self): + """Client sets server_hostname to the host in the WebSocket URI.""" + # This is part of the documented behavior of unix_connect(). + with temp_unix_socket_path() as path: + with run_unix_server(path, ssl_context=SERVER_CONTEXT): + with run_unix_client( + path, + ssl_context=CLIENT_CONTEXT, + uri="wss://overridden/", + ) as client: + self.assertEqual(client.socket.server_hostname, "overridden") + + +class ClientUsageErrorsTests(unittest.TestCase): + def test_ssl_context_without_secure_uri(self): + """Client rejects ssl_context when URI isn't secure.""" + with self.assertRaisesRegex( + TypeError, + "ssl_context argument is incompatible with a ws:// URI", + ): + connect("ws://localhost/", ssl_context=CLIENT_CONTEXT) + + def test_unix_without_path_or_sock(self): + """Unix client requires path when sock isn't provided.""" + with self.assertRaisesRegex( + TypeError, + "missing path argument", + ): + unix_connect() + + def test_unix_with_path_and_sock(self): + """Unix client rejects path when sock is provided.""" + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.addCleanup(sock.close) + with self.assertRaisesRegex( + TypeError, + "path and sock arguments are incompatible", + ): + unix_connect(path="/", sock=sock) + + def test_invalid_subprotocol(self): + """Client rejects single value of subprotocols.""" + with self.assertRaisesRegex( + TypeError, + "subprotocols must be a list", + ): + connect("ws://localhost/", subprotocols="chat") + + def test_unsupported_compression(self): + """Client rejects incorrect value of compression.""" + with self.assertRaisesRegex( + ValueError, + "unsupported compression: False", + ): + connect("ws://localhost/", compression=False) diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/sync/test_connection.py b/tests/wpt/tests/tools/third_party/websockets/tests/sync/test_connection.py new file mode 100644 index 00000000000..63544d4add7 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/sync/test_connection.py @@ -0,0 +1,752 @@ +import contextlib +import logging +import platform +import socket +import sys +import threading +import time +import unittest +import uuid +from unittest.mock import patch + +from websockets.exceptions import ConnectionClosedError, ConnectionClosedOK +from websockets.frames import CloseCode, Frame, Opcode +from websockets.protocol import CLIENT, SERVER, Protocol +from websockets.sync.connection import * + +from ..protocol import RecordingProtocol +from ..utils import MS +from .connection import InterceptingConnection + + +# Connection implements symmetrical behavior between clients and servers. +# All tests run on the client side and the server side to validate this. + + +class ClientConnectionTests(unittest.TestCase): + LOCAL = CLIENT + REMOTE = SERVER + + def setUp(self): + socket_, remote_socket = socket.socketpair() + protocol = Protocol(self.LOCAL) + remote_protocol = RecordingProtocol(self.REMOTE) + self.connection = Connection(socket_, protocol, close_timeout=2 * MS) + self.remote_connection = InterceptingConnection(remote_socket, remote_protocol) + + def tearDown(self): + self.remote_connection.close() + self.connection.close() + + # Test helpers built upon RecordingProtocol and InterceptingConnection. + + def assertFrameSent(self, frame): + """Check that a single frame was sent.""" + time.sleep(MS) # let the remote side process messages + self.assertEqual(self.remote_connection.protocol.get_frames_rcvd(), [frame]) + + def assertNoFrameSent(self): + """Check that no frame was sent.""" + time.sleep(MS) # let the remote side process messages + self.assertEqual(self.remote_connection.protocol.get_frames_rcvd(), []) + + @contextlib.contextmanager + def delay_frames_rcvd(self, delay): + """Delay frames before they're received by the connection.""" + with self.remote_connection.delay_frames_sent(delay): + yield + time.sleep(MS) # let the remote side process messages + + @contextlib.contextmanager + def delay_eof_rcvd(self, delay): + """Delay EOF before it's received by the connection.""" + with self.remote_connection.delay_eof_sent(delay): + yield + time.sleep(MS) # let the remote side process messages + + @contextlib.contextmanager + def drop_frames_rcvd(self): + """Drop frames before they're received by the connection.""" + with self.remote_connection.drop_frames_sent(): + yield + time.sleep(MS) # let the remote side process messages + + @contextlib.contextmanager + def drop_eof_rcvd(self): + """Drop EOF before it's received by the connection.""" + with self.remote_connection.drop_eof_sent(): + yield + time.sleep(MS) # let the remote side process messages + + # Test __enter__ and __exit__. + + def test_enter(self): + """__enter__ returns the connection itself.""" + with self.connection as connection: + self.assertIs(connection, self.connection) + + def test_exit(self): + """__exit__ closes the connection with code 1000.""" + with self.connection: + self.assertNoFrameSent() + self.assertFrameSent(Frame(Opcode.CLOSE, b"\x03\xe8")) + + def test_exit_with_exception(self): + """__exit__ with an exception closes the connection with code 1011.""" + with self.assertRaises(RuntimeError): + with self.connection: + raise RuntimeError + self.assertFrameSent(Frame(Opcode.CLOSE, b"\x03\xf3")) + + # Test __iter__. + + def test_iter_text(self): + """__iter__ yields text messages.""" + iterator = iter(self.connection) + self.remote_connection.send("😀") + self.assertEqual(next(iterator), "😀") + self.remote_connection.send("😀") + self.assertEqual(next(iterator), "😀") + + def test_iter_binary(self): + """__iter__ yields binary messages.""" + iterator = iter(self.connection) + self.remote_connection.send(b"\x01\x02\xfe\xff") + self.assertEqual(next(iterator), b"\x01\x02\xfe\xff") + self.remote_connection.send(b"\x01\x02\xfe\xff") + self.assertEqual(next(iterator), b"\x01\x02\xfe\xff") + + def test_iter_mixed(self): + """__iter__ yields a mix of text and binary messages.""" + iterator = iter(self.connection) + self.remote_connection.send("😀") + self.assertEqual(next(iterator), "😀") + self.remote_connection.send(b"\x01\x02\xfe\xff") + self.assertEqual(next(iterator), b"\x01\x02\xfe\xff") + + def test_iter_connection_closed_ok(self): + """__iter__ terminates after a normal closure.""" + iterator = iter(self.connection) + self.remote_connection.close() + with self.assertRaises(StopIteration): + next(iterator) + + def test_iter_connection_closed_error(self): + """__iter__ raises ConnnectionClosedError after an error.""" + iterator = iter(self.connection) + self.remote_connection.close(code=CloseCode.INTERNAL_ERROR) + with self.assertRaises(ConnectionClosedError): + next(iterator) + + # Test recv. + + def test_recv_text(self): + """recv receives a text message.""" + self.remote_connection.send("😀") + self.assertEqual(self.connection.recv(), "😀") + + def test_recv_binary(self): + """recv receives a binary message.""" + self.remote_connection.send(b"\x01\x02\xfe\xff") + self.assertEqual(self.connection.recv(), b"\x01\x02\xfe\xff") + + def test_recv_fragmented_text(self): + """recv receives a fragmented text message.""" + self.remote_connection.send(["😀", "😀"]) + self.assertEqual(self.connection.recv(), "😀😀") + + def test_recv_fragmented_binary(self): + """recv receives a fragmented binary message.""" + self.remote_connection.send([b"\x01\x02", b"\xfe\xff"]) + self.assertEqual(self.connection.recv(), b"\x01\x02\xfe\xff") + + def test_recv_connection_closed_ok(self): + """recv raises ConnectionClosedOK after a normal closure.""" + self.remote_connection.close() + with self.assertRaises(ConnectionClosedOK): + self.connection.recv() + + def test_recv_connection_closed_error(self): + """recv raises ConnectionClosedError after an error.""" + self.remote_connection.close(code=CloseCode.INTERNAL_ERROR) + with self.assertRaises(ConnectionClosedError): + self.connection.recv() + + def test_recv_during_recv(self): + """recv raises RuntimeError when called concurrently with itself.""" + recv_thread = threading.Thread(target=self.connection.recv) + recv_thread.start() + + with self.assertRaisesRegex( + RuntimeError, + "cannot call recv while another thread " + "is already running recv or recv_streaming", + ): + self.connection.recv() + + self.remote_connection.send("") + recv_thread.join() + + def test_recv_during_recv_streaming(self): + """recv raises RuntimeError when called concurrently with recv_streaming.""" + recv_streaming_thread = threading.Thread( + target=lambda: list(self.connection.recv_streaming()) + ) + recv_streaming_thread.start() + + with self.assertRaisesRegex( + RuntimeError, + "cannot call recv while another thread " + "is already running recv or recv_streaming", + ): + self.connection.recv() + + self.remote_connection.send("") + recv_streaming_thread.join() + + # Test recv_streaming. + + def test_recv_streaming_text(self): + """recv_streaming receives a text message.""" + self.remote_connection.send("😀") + self.assertEqual( + list(self.connection.recv_streaming()), + ["😀"], + ) + + def test_recv_streaming_binary(self): + """recv_streaming receives a binary message.""" + self.remote_connection.send(b"\x01\x02\xfe\xff") + self.assertEqual( + list(self.connection.recv_streaming()), + [b"\x01\x02\xfe\xff"], + ) + + def test_recv_streaming_fragmented_text(self): + """recv_streaming receives a fragmented text message.""" + self.remote_connection.send(["😀", "😀"]) + # websockets sends an trailing empty fragment. That's an implementation detail. + self.assertEqual( + list(self.connection.recv_streaming()), + ["😀", "😀", ""], + ) + + def test_recv_streaming_fragmented_binary(self): + """recv_streaming receives a fragmented binary message.""" + self.remote_connection.send([b"\x01\x02", b"\xfe\xff"]) + # websockets sends an trailing empty fragment. That's an implementation detail. + self.assertEqual( + list(self.connection.recv_streaming()), + [b"\x01\x02", b"\xfe\xff", b""], + ) + + def test_recv_streaming_connection_closed_ok(self): + """recv_streaming raises ConnectionClosedOK after a normal closure.""" + self.remote_connection.close() + with self.assertRaises(ConnectionClosedOK): + list(self.connection.recv_streaming()) + + def test_recv_streaming_connection_closed_error(self): + """recv_streaming raises ConnectionClosedError after an error.""" + self.remote_connection.close(code=CloseCode.INTERNAL_ERROR) + with self.assertRaises(ConnectionClosedError): + list(self.connection.recv_streaming()) + + def test_recv_streaming_during_recv(self): + """recv_streaming raises RuntimeError when called concurrently with recv.""" + recv_thread = threading.Thread(target=self.connection.recv) + recv_thread.start() + + with self.assertRaisesRegex( + RuntimeError, + "cannot call recv_streaming while another thread " + "is already running recv or recv_streaming", + ): + list(self.connection.recv_streaming()) + + self.remote_connection.send("") + recv_thread.join() + + def test_recv_streaming_during_recv_streaming(self): + """recv_streaming raises RuntimeError when called concurrently with itself.""" + recv_streaming_thread = threading.Thread( + target=lambda: list(self.connection.recv_streaming()) + ) + recv_streaming_thread.start() + + with self.assertRaisesRegex( + RuntimeError, + r"cannot call recv_streaming while another thread " + r"is already running recv or recv_streaming", + ): + list(self.connection.recv_streaming()) + + self.remote_connection.send("") + recv_streaming_thread.join() + + # Test send. + + def test_send_text(self): + """send sends a text message.""" + self.connection.send("😀") + self.assertEqual(self.remote_connection.recv(), "😀") + + def test_send_binary(self): + """send sends a binary message.""" + self.connection.send(b"\x01\x02\xfe\xff") + self.assertEqual(self.remote_connection.recv(), b"\x01\x02\xfe\xff") + + def test_send_fragmented_text(self): + """send sends a fragmented text message.""" + self.connection.send(["😀", "😀"]) + # websockets sends an trailing empty fragment. That's an implementation detail. + self.assertEqual( + list(self.remote_connection.recv_streaming()), + ["😀", "😀", ""], + ) + + def test_send_fragmented_binary(self): + """send sends a fragmented binary message.""" + self.connection.send([b"\x01\x02", b"\xfe\xff"]) + # websockets sends an trailing empty fragment. That's an implementation detail. + self.assertEqual( + list(self.remote_connection.recv_streaming()), + [b"\x01\x02", b"\xfe\xff", b""], + ) + + def test_send_connection_closed_ok(self): + """send raises ConnectionClosedOK after a normal closure.""" + self.remote_connection.close() + with self.assertRaises(ConnectionClosedOK): + self.connection.send("😀") + + def test_send_connection_closed_error(self): + """send raises ConnectionClosedError after an error.""" + self.remote_connection.close(code=CloseCode.INTERNAL_ERROR) + with self.assertRaises(ConnectionClosedError): + self.connection.send("😀") + + def test_send_during_send(self): + """send raises RuntimeError when called concurrently with itself.""" + recv_thread = threading.Thread(target=self.remote_connection.recv) + recv_thread.start() + + send_gate = threading.Event() + exit_gate = threading.Event() + + def fragments(): + yield "😀" + send_gate.set() + exit_gate.wait() + yield "😀" + + send_thread = threading.Thread( + target=self.connection.send, + args=(fragments(),), + ) + send_thread.start() + + send_gate.wait() + # The check happens in four code paths, depending on the argument. + for message in [ + "😀", + b"\x01\x02\xfe\xff", + ["😀", "😀"], + [b"\x01\x02", b"\xfe\xff"], + ]: + with self.subTest(message=message): + with self.assertRaisesRegex( + RuntimeError, + "cannot call send while another thread is already running send", + ): + self.connection.send(message) + + exit_gate.set() + send_thread.join() + recv_thread.join() + + def test_send_empty_iterable(self): + """send does nothing when called with an empty iterable.""" + self.connection.send([]) + self.connection.close() + self.assertEqual(list(iter(self.remote_connection)), []) + + def test_send_mixed_iterable(self): + """send raises TypeError when called with an iterable of inconsistent types.""" + with self.assertRaises(TypeError): + self.connection.send(["😀", b"\xfe\xff"]) + + def test_send_unsupported_iterable(self): + """send raises TypeError when called with an iterable of unsupported type.""" + with self.assertRaises(TypeError): + self.connection.send([None]) + + def test_send_dict(self): + """send raises TypeError when called with a dict.""" + with self.assertRaises(TypeError): + self.connection.send({"type": "object"}) + + def test_send_unsupported_type(self): + """send raises TypeError when called with an unsupported type.""" + with self.assertRaises(TypeError): + self.connection.send(None) + + # Test close. + + def test_close(self): + """close sends a close frame.""" + self.connection.close() + self.assertFrameSent(Frame(Opcode.CLOSE, b"\x03\xe8")) + + def test_close_explicit_code_reason(self): + """close sends a close frame with a given code and reason.""" + self.connection.close(CloseCode.GOING_AWAY, "bye!") + self.assertFrameSent(Frame(Opcode.CLOSE, b"\x03\xe9bye!")) + + def test_close_waits_for_close_frame(self): + """close waits for a close frame (then EOF) before returning.""" + with self.delay_frames_rcvd(MS): + self.connection.close() + + with self.assertRaises(ConnectionClosedOK) as raised: + self.connection.recv() + + exc = raised.exception + self.assertEqual(str(exc), "sent 1000 (OK); then received 1000 (OK)") + self.assertIsNone(exc.__cause__) + + def test_close_waits_for_connection_closed(self): + """close waits for EOF before returning.""" + if self.LOCAL is SERVER: + self.skipTest("only relevant on the client-side") + + with self.delay_eof_rcvd(MS): + self.connection.close() + + with self.assertRaises(ConnectionClosedOK) as raised: + self.connection.recv() + + exc = raised.exception + self.assertEqual(str(exc), "sent 1000 (OK); then received 1000 (OK)") + self.assertIsNone(exc.__cause__) + + def test_close_timeout_waiting_for_close_frame(self): + """close times out if no close frame is received.""" + with self.drop_eof_rcvd(), self.drop_frames_rcvd(): + self.connection.close() + + with self.assertRaises(ConnectionClosedError) as raised: + self.connection.recv() + + exc = raised.exception + self.assertEqual(str(exc), "sent 1000 (OK); no close frame received") + self.assertIsInstance(exc.__cause__, TimeoutError) + + def test_close_timeout_waiting_for_connection_closed(self): + """close times out if EOF isn't received.""" + if self.LOCAL is SERVER: + self.skipTest("only relevant on the client-side") + + with self.drop_eof_rcvd(): + self.connection.close() + + with self.assertRaises(ConnectionClosedOK) as raised: + self.connection.recv() + + exc = raised.exception + self.assertEqual(str(exc), "sent 1000 (OK); then received 1000 (OK)") + # Remove socket.timeout when dropping Python < 3.10. + self.assertIsInstance(exc.__cause__, (socket.timeout, TimeoutError)) + + def test_close_waits_for_recv(self): + self.remote_connection.send("😀") + + close_thread = threading.Thread(target=self.connection.close) + close_thread.start() + + # Let close() initiate the closing handshake and send a close frame. + time.sleep(MS) + self.assertTrue(close_thread.is_alive()) + + # Connection isn't closed yet. + self.connection.recv() + + # Let close() receive a close frame and finish the closing handshake. + time.sleep(MS) + self.assertFalse(close_thread.is_alive()) + + # Connection is closed now. + with self.assertRaises(ConnectionClosedOK) as raised: + self.connection.recv() + + exc = raised.exception + self.assertEqual(str(exc), "sent 1000 (OK); then received 1000 (OK)") + self.assertIsNone(exc.__cause__) + + def test_close_timeout_waiting_for_recv(self): + self.remote_connection.send("😀") + + close_thread = threading.Thread(target=self.connection.close) + close_thread.start() + + # Let close() time out during the closing handshake. + time.sleep(3 * MS) + self.assertFalse(close_thread.is_alive()) + + # Connection is closed now. + with self.assertRaises(ConnectionClosedError) as raised: + self.connection.recv() + + exc = raised.exception + self.assertEqual(str(exc), "sent 1000 (OK); no close frame received") + self.assertIsInstance(exc.__cause__, TimeoutError) + + def test_close_idempotency(self): + """close does nothing if the connection is already closed.""" + self.connection.close() + self.assertFrameSent(Frame(Opcode.CLOSE, b"\x03\xe8")) + + self.connection.close() + self.assertNoFrameSent() + + @unittest.skipIf( + platform.python_implementation() == "PyPy", + "this test fails randomly due to a bug in PyPy", # see #1314 for details + ) + def test_close_idempotency_race_condition(self): + """close waits if the connection is already closing.""" + + self.connection.close_timeout = 5 * MS + + def closer(): + with self.delay_frames_rcvd(3 * MS): + self.connection.close() + + close_thread = threading.Thread(target=closer) + close_thread.start() + + # Let closer() initiate the closing handshake and send a close frame. + time.sleep(MS) + self.assertFrameSent(Frame(Opcode.CLOSE, b"\x03\xe8")) + + # Connection isn't closed yet. + with self.assertRaises(TimeoutError): + self.connection.recv(timeout=0) + + self.connection.close() + self.assertNoFrameSent() + + # Connection is closed now. + with self.assertRaises(ConnectionClosedOK): + self.connection.recv(timeout=0) + + close_thread.join() + + def test_close_during_send(self): + """close fails the connection when called concurrently with send.""" + close_gate = threading.Event() + exit_gate = threading.Event() + + def closer(): + close_gate.wait() + self.connection.close() + exit_gate.set() + + def fragments(): + yield "😀" + close_gate.set() + exit_gate.wait() + yield "😀" + + close_thread = threading.Thread(target=closer) + close_thread.start() + + with self.assertRaises(ConnectionClosedError) as raised: + self.connection.send(fragments()) + + exc = raised.exception + self.assertEqual( + str(exc), + "sent 1011 (internal error) close during fragmented message; " + "no close frame received", + ) + self.assertIsNone(exc.__cause__) + + close_thread.join() + + # Test ping. + + @patch("random.getrandbits") + def test_ping(self, getrandbits): + """ping sends a ping frame with a random payload.""" + getrandbits.return_value = 1918987876 + self.connection.ping() + getrandbits.assert_called_once_with(32) + self.assertFrameSent(Frame(Opcode.PING, b"rand")) + + def test_ping_explicit_text(self): + """ping sends a ping frame with a payload provided as text.""" + self.connection.ping("ping") + self.assertFrameSent(Frame(Opcode.PING, b"ping")) + + def test_ping_explicit_binary(self): + """ping sends a ping frame with a payload provided as binary.""" + self.connection.ping(b"ping") + self.assertFrameSent(Frame(Opcode.PING, b"ping")) + + def test_ping_duplicate_payload(self): + """ping rejects the same payload until receiving the pong.""" + with self.remote_connection.protocol_mutex: # block response to ping + pong_waiter = self.connection.ping("idem") + with self.assertRaisesRegex( + RuntimeError, + "already waiting for a pong with the same data", + ): + self.connection.ping("idem") + self.assertTrue(pong_waiter.wait(MS)) + self.connection.ping("idem") # doesn't raise an exception + + def test_acknowledge_ping(self): + """ping is acknowledged by a pong with the same payload.""" + with self.drop_frames_rcvd(): + pong_waiter = self.connection.ping("this") + self.assertFalse(pong_waiter.wait(MS)) + self.remote_connection.pong("this") + self.assertTrue(pong_waiter.wait(MS)) + + def test_acknowledge_ping_non_matching_pong(self): + """ping isn't acknowledged by a pong with a different payload.""" + with self.drop_frames_rcvd(): + pong_waiter = self.connection.ping("this") + self.remote_connection.pong("that") + self.assertFalse(pong_waiter.wait(MS)) + + def test_acknowledge_previous_ping(self): + """ping is acknowledged by a pong with the same payload as a later ping.""" + with self.drop_frames_rcvd(): + pong_waiter = self.connection.ping("this") + self.connection.ping("that") + self.remote_connection.pong("that") + self.assertTrue(pong_waiter.wait(MS)) + + # Test pong. + + def test_pong(self): + """pong sends a pong frame.""" + self.connection.pong() + self.assertFrameSent(Frame(Opcode.PONG, b"")) + + def test_pong_explicit_text(self): + """pong sends a pong frame with a payload provided as text.""" + self.connection.pong("pong") + self.assertFrameSent(Frame(Opcode.PONG, b"pong")) + + def test_pong_explicit_binary(self): + """pong sends a pong frame with a payload provided as binary.""" + self.connection.pong(b"pong") + self.assertFrameSent(Frame(Opcode.PONG, b"pong")) + + # Test attributes. + + def test_id(self): + """Connection has an id attribute.""" + self.assertIsInstance(self.connection.id, uuid.UUID) + + def test_logger(self): + """Connection has a logger attribute.""" + self.assertIsInstance(self.connection.logger, logging.LoggerAdapter) + + def test_local_address(self): + """Connection has a local_address attribute.""" + self.assertIsNotNone(self.connection.local_address) + + def test_remote_address(self): + """Connection has a remote_address attribute.""" + self.assertIsNotNone(self.connection.remote_address) + + def test_request(self): + """Connection has a request attribute.""" + self.assertIsNone(self.connection.request) + + def test_response(self): + """Connection has a response attribute.""" + self.assertIsNone(self.connection.response) + + def test_subprotocol(self): + """Connection has a subprotocol attribute.""" + self.assertIsNone(self.connection.subprotocol) + + # Test reporting of network errors. + + @unittest.skipUnless(sys.platform == "darwin", "works only on BSD") + def test_reading_in_recv_events_fails(self): + """Error when reading incoming frames is correctly reported.""" + # Inject a fault by closing the socket. This works only on BSD. + # I cannot find a way to achieve the same effect on Linux. + self.connection.socket.close() + # The connection closed exception reports the injected fault. + with self.assertRaises(ConnectionClosedError) as raised: + self.connection.recv() + self.assertIsInstance(raised.exception.__cause__, IOError) + + def test_writing_in_recv_events_fails(self): + """Error when responding to incoming frames is correctly reported.""" + # Inject a fault by shutting down the socket for writing — but not by + # closing it because that would terminate the connection. + self.connection.socket.shutdown(socket.SHUT_WR) + # Receive a ping. Responding with a pong will fail. + self.remote_connection.ping() + # The connection closed exception reports the injected fault. + with self.assertRaises(ConnectionClosedError) as raised: + self.connection.recv() + self.assertIsInstance(raised.exception.__cause__, BrokenPipeError) + + def test_writing_in_send_context_fails(self): + """Error when sending outgoing frame is correctly reported.""" + # Inject a fault by shutting down the socket for writing — but not by + # closing it because that would terminate the connection. + self.connection.socket.shutdown(socket.SHUT_WR) + # Sending a pong will fail. + # The connection closed exception reports the injected fault. + with self.assertRaises(ConnectionClosedError) as raised: + self.connection.pong() + self.assertIsInstance(raised.exception.__cause__, BrokenPipeError) + + # Test safety nets — catching all exceptions in case of bugs. + + @patch("websockets.protocol.Protocol.events_received") + def test_unexpected_failure_in_recv_events(self, events_received): + """Unexpected internal error in recv_events() is correctly reported.""" + # Inject a fault in a random call in recv_events(). + # This test is tightly coupled to the implementation. + events_received.side_effect = AssertionError + # Receive a message to trigger the fault. + self.remote_connection.send("😀") + + with self.assertRaises(ConnectionClosedError) as raised: + self.connection.recv() + + exc = raised.exception + self.assertEqual(str(exc), "no close frame received or sent") + self.assertIsInstance(exc.__cause__, AssertionError) + + @patch("websockets.protocol.Protocol.send_text") + def test_unexpected_failure_in_send_context(self, send_text): + """Unexpected internal error in send_context() is correctly reported.""" + # Inject a fault in a random call in send_context(). + # This test is tightly coupled to the implementation. + send_text.side_effect = AssertionError + + # Send a message to trigger the fault. + # The connection closed exception reports the injected fault. + with self.assertRaises(ConnectionClosedError) as raised: + self.connection.send("😀") + + exc = raised.exception + self.assertEqual(str(exc), "no close frame received or sent") + self.assertIsInstance(exc.__cause__, AssertionError) + + +class ServerConnectionTests(ClientConnectionTests): + LOCAL = SERVER + REMOTE = CLIENT diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/sync/test_messages.py b/tests/wpt/tests/tools/third_party/websockets/tests/sync/test_messages.py new file mode 100644 index 00000000000..825eb879740 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/sync/test_messages.py @@ -0,0 +1,479 @@ +import time + +from websockets.frames import OP_BINARY, OP_CONT, OP_PING, OP_PONG, OP_TEXT, Frame +from websockets.sync.messages import * + +from ..utils import MS +from .utils import ThreadTestCase + + +class AssemblerTests(ThreadTestCase): + """ + Tests in this class interact a lot with hidden synchronization mechanisms: + + - get() / get_iter() and put() must run in separate threads when a final + frame is set because put() waits for get() / get_iter() to fetch the + message before returning. + + - run_in_thread() lets its target run before yielding back control on entry, + which guarantees the intended execution order of test cases. + + - run_in_thread() waits for its target to finish running before yielding + back control on exit, which allows making assertions immediately. + + - When the main thread performs actions that let another thread progress, it + must wait before making assertions, to avoid depending on scheduling. + + """ + + def setUp(self): + self.assembler = Assembler() + + def tearDown(self): + """ + Check that the assembler goes back to its default state after each test. + + This removes the need for testing various sequences. + + """ + self.assertFalse(self.assembler.mutex.locked()) + self.assertFalse(self.assembler.get_in_progress) + self.assertFalse(self.assembler.put_in_progress) + if not self.assembler.closed: + self.assertFalse(self.assembler.message_complete.is_set()) + self.assertFalse(self.assembler.message_fetched.is_set()) + self.assertIsNone(self.assembler.decoder) + self.assertEqual(self.assembler.chunks, []) + self.assertIsNone(self.assembler.chunks_queue) + + # Test get + + def test_get_text_message_already_received(self): + """get returns a text message that is already received.""" + + def putter(): + self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9")) + + with self.run_in_thread(putter): + message = self.assembler.get() + + self.assertEqual(message, "café") + + def test_get_binary_message_already_received(self): + """get returns a binary message that is already received.""" + + def putter(): + self.assembler.put(Frame(OP_BINARY, b"tea")) + + with self.run_in_thread(putter): + message = self.assembler.get() + + self.assertEqual(message, b"tea") + + def test_get_text_message_not_received_yet(self): + """get returns a text message when it is received.""" + message = None + + def getter(): + nonlocal message + message = self.assembler.get() + + with self.run_in_thread(getter): + self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9")) + + self.assertEqual(message, "café") + + def test_get_binary_message_not_received_yet(self): + """get returns a binary message when it is received.""" + message = None + + def getter(): + nonlocal message + message = self.assembler.get() + + with self.run_in_thread(getter): + self.assembler.put(Frame(OP_BINARY, b"tea")) + + self.assertEqual(message, b"tea") + + def test_get_fragmented_text_message_already_received(self): + """get reassembles a fragmented a text message that is already received.""" + + def putter(): + self.assembler.put(Frame(OP_TEXT, b"ca", fin=False)) + self.assembler.put(Frame(OP_CONT, b"f\xc3", fin=False)) + self.assembler.put(Frame(OP_CONT, b"\xa9")) + + with self.run_in_thread(putter): + message = self.assembler.get() + + self.assertEqual(message, "café") + + def test_get_fragmented_binary_message_already_received(self): + """get reassembles a fragmented binary message that is already received.""" + + def putter(): + self.assembler.put(Frame(OP_BINARY, b"t", fin=False)) + self.assembler.put(Frame(OP_CONT, b"e", fin=False)) + self.assembler.put(Frame(OP_CONT, b"a")) + + with self.run_in_thread(putter): + message = self.assembler.get() + + self.assertEqual(message, b"tea") + + def test_get_fragmented_text_message_being_received(self): + """get reassembles a fragmented text message that is partially received.""" + message = None + + def getter(): + nonlocal message + message = self.assembler.get() + + self.assembler.put(Frame(OP_TEXT, b"ca", fin=False)) + with self.run_in_thread(getter): + self.assembler.put(Frame(OP_CONT, b"f\xc3", fin=False)) + self.assembler.put(Frame(OP_CONT, b"\xa9")) + + self.assertEqual(message, "café") + + def test_get_fragmented_binary_message_being_received(self): + """get reassembles a fragmented binary message that is partially received.""" + message = None + + def getter(): + nonlocal message + message = self.assembler.get() + + self.assembler.put(Frame(OP_BINARY, b"t", fin=False)) + with self.run_in_thread(getter): + self.assembler.put(Frame(OP_CONT, b"e", fin=False)) + self.assembler.put(Frame(OP_CONT, b"a")) + + self.assertEqual(message, b"tea") + + def test_get_fragmented_text_message_not_received_yet(self): + """get reassembles a fragmented text message when it is received.""" + message = None + + def getter(): + nonlocal message + message = self.assembler.get() + + with self.run_in_thread(getter): + self.assembler.put(Frame(OP_TEXT, b"ca", fin=False)) + self.assembler.put(Frame(OP_CONT, b"f\xc3", fin=False)) + self.assembler.put(Frame(OP_CONT, b"\xa9")) + + self.assertEqual(message, "café") + + def test_get_fragmented_binary_message_not_received_yet(self): + """get reassembles a fragmented binary message when it is received.""" + message = None + + def getter(): + nonlocal message + message = self.assembler.get() + + with self.run_in_thread(getter): + self.assembler.put(Frame(OP_BINARY, b"t", fin=False)) + self.assembler.put(Frame(OP_CONT, b"e", fin=False)) + self.assembler.put(Frame(OP_CONT, b"a")) + + self.assertEqual(message, b"tea") + + # Test get_iter + + def test_get_iter_text_message_already_received(self): + """get_iter yields a text message that is already received.""" + + def putter(): + self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9")) + + with self.run_in_thread(putter): + fragments = list(self.assembler.get_iter()) + + self.assertEqual(fragments, ["café"]) + + def test_get_iter_binary_message_already_received(self): + """get_iter yields a binary message that is already received.""" + + def putter(): + self.assembler.put(Frame(OP_BINARY, b"tea")) + + with self.run_in_thread(putter): + fragments = list(self.assembler.get_iter()) + + self.assertEqual(fragments, [b"tea"]) + + def test_get_iter_text_message_not_received_yet(self): + """get_iter yields a text message when it is received.""" + fragments = [] + + def getter(): + for fragment in self.assembler.get_iter(): + fragments.append(fragment) + + with self.run_in_thread(getter): + self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9")) + + self.assertEqual(fragments, ["café"]) + + def test_get_iter_binary_message_not_received_yet(self): + """get_iter yields a binary message when it is received.""" + fragments = [] + + def getter(): + for fragment in self.assembler.get_iter(): + fragments.append(fragment) + + with self.run_in_thread(getter): + self.assembler.put(Frame(OP_BINARY, b"tea")) + + self.assertEqual(fragments, [b"tea"]) + + def test_get_iter_fragmented_text_message_already_received(self): + """get_iter yields a fragmented text message that is already received.""" + + def putter(): + self.assembler.put(Frame(OP_TEXT, b"ca", fin=False)) + self.assembler.put(Frame(OP_CONT, b"f\xc3", fin=False)) + self.assembler.put(Frame(OP_CONT, b"\xa9")) + + with self.run_in_thread(putter): + fragments = list(self.assembler.get_iter()) + + self.assertEqual(fragments, ["ca", "f", "é"]) + + def test_get_iter_fragmented_binary_message_already_received(self): + """get_iter yields a fragmented binary message that is already received.""" + + def putter(): + self.assembler.put(Frame(OP_BINARY, b"t", fin=False)) + self.assembler.put(Frame(OP_CONT, b"e", fin=False)) + self.assembler.put(Frame(OP_CONT, b"a")) + + with self.run_in_thread(putter): + fragments = list(self.assembler.get_iter()) + + self.assertEqual(fragments, [b"t", b"e", b"a"]) + + def test_get_iter_fragmented_text_message_being_received(self): + """get_iter yields a fragmented text message that is partially received.""" + fragments = [] + + def getter(): + for fragment in self.assembler.get_iter(): + fragments.append(fragment) + + self.assembler.put(Frame(OP_TEXT, b"ca", fin=False)) + with self.run_in_thread(getter): + self.assertEqual(fragments, ["ca"]) + self.assembler.put(Frame(OP_CONT, b"f\xc3", fin=False)) + time.sleep(MS) + self.assertEqual(fragments, ["ca", "f"]) + self.assembler.put(Frame(OP_CONT, b"\xa9")) + + self.assertEqual(fragments, ["ca", "f", "é"]) + + def test_get_iter_fragmented_binary_message_being_received(self): + """get_iter yields a fragmented binary message that is partially received.""" + fragments = [] + + def getter(): + for fragment in self.assembler.get_iter(): + fragments.append(fragment) + + self.assembler.put(Frame(OP_BINARY, b"t", fin=False)) + with self.run_in_thread(getter): + self.assertEqual(fragments, [b"t"]) + self.assembler.put(Frame(OP_CONT, b"e", fin=False)) + time.sleep(MS) + self.assertEqual(fragments, [b"t", b"e"]) + self.assembler.put(Frame(OP_CONT, b"a")) + + self.assertEqual(fragments, [b"t", b"e", b"a"]) + + def test_get_iter_fragmented_text_message_not_received_yet(self): + """get_iter yields a fragmented text message when it is received.""" + fragments = [] + + def getter(): + for fragment in self.assembler.get_iter(): + fragments.append(fragment) + + with self.run_in_thread(getter): + self.assembler.put(Frame(OP_TEXT, b"ca", fin=False)) + time.sleep(MS) + self.assertEqual(fragments, ["ca"]) + self.assembler.put(Frame(OP_CONT, b"f\xc3", fin=False)) + time.sleep(MS) + self.assertEqual(fragments, ["ca", "f"]) + self.assembler.put(Frame(OP_CONT, b"\xa9")) + + self.assertEqual(fragments, ["ca", "f", "é"]) + + def test_get_iter_fragmented_binary_message_not_received_yet(self): + """get_iter yields a fragmented binary message when it is received.""" + fragments = [] + + def getter(): + for fragment in self.assembler.get_iter(): + fragments.append(fragment) + + with self.run_in_thread(getter): + self.assembler.put(Frame(OP_BINARY, b"t", fin=False)) + time.sleep(MS) + self.assertEqual(fragments, [b"t"]) + self.assembler.put(Frame(OP_CONT, b"e", fin=False)) + time.sleep(MS) + self.assertEqual(fragments, [b"t", b"e"]) + self.assembler.put(Frame(OP_CONT, b"a")) + + self.assertEqual(fragments, [b"t", b"e", b"a"]) + + # Test timeouts + + def test_get_with_timeout_completes(self): + """get returns a message when it is received before the timeout.""" + + def putter(): + self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9")) + + with self.run_in_thread(putter): + message = self.assembler.get(MS) + + self.assertEqual(message, "café") + + def test_get_with_timeout_times_out(self): + """get raises TimeoutError when no message is received before the timeout.""" + with self.assertRaises(TimeoutError): + self.assembler.get(MS) + + # Test control frames + + def test_control_frame_before_message_is_ignored(self): + """get ignores control frames between messages.""" + + def putter(): + self.assembler.put(Frame(OP_PING, b"")) + self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9")) + + with self.run_in_thread(putter): + message = self.assembler.get() + + self.assertEqual(message, "café") + + def test_control_frame_in_fragmented_message_is_ignored(self): + """get ignores control frames within fragmented messages.""" + + def putter(): + self.assembler.put(Frame(OP_BINARY, b"t", fin=False)) + self.assembler.put(Frame(OP_PING, b"")) + self.assembler.put(Frame(OP_CONT, b"e", fin=False)) + self.assembler.put(Frame(OP_PONG, b"")) + self.assembler.put(Frame(OP_CONT, b"a")) + + with self.run_in_thread(putter): + message = self.assembler.get() + + self.assertEqual(message, b"tea") + + # Test concurrency + + def test_get_fails_when_get_is_running(self): + """get cannot be called concurrently with itself.""" + with self.run_in_thread(self.assembler.get): + with self.assertRaises(RuntimeError): + self.assembler.get() + self.assembler.put(Frame(OP_TEXT, b"")) # unlock other thread + + def test_get_fails_when_get_iter_is_running(self): + """get cannot be called concurrently with get_iter.""" + with self.run_in_thread(lambda: list(self.assembler.get_iter())): + with self.assertRaises(RuntimeError): + self.assembler.get() + self.assembler.put(Frame(OP_TEXT, b"")) # unlock other thread + + def test_get_iter_fails_when_get_is_running(self): + """get_iter cannot be called concurrently with get.""" + with self.run_in_thread(self.assembler.get): + with self.assertRaises(RuntimeError): + list(self.assembler.get_iter()) + self.assembler.put(Frame(OP_TEXT, b"")) # unlock other thread + + def test_get_iter_fails_when_get_iter_is_running(self): + """get_iter cannot be called concurrently with itself.""" + with self.run_in_thread(lambda: list(self.assembler.get_iter())): + with self.assertRaises(RuntimeError): + list(self.assembler.get_iter()) + self.assembler.put(Frame(OP_TEXT, b"")) # unlock other thread + + def test_put_fails_when_put_is_running(self): + """put cannot be called concurrently with itself.""" + + def putter(): + self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9")) + + with self.run_in_thread(putter): + with self.assertRaises(RuntimeError): + self.assembler.put(Frame(OP_BINARY, b"tea")) + self.assembler.get() # unblock other thread + + # Test termination + + def test_get_fails_when_interrupted_by_close(self): + """get raises EOFError when close is called.""" + + def closer(): + time.sleep(2 * MS) + self.assembler.close() + + with self.run_in_thread(closer): + with self.assertRaises(EOFError): + self.assembler.get() + + def test_get_iter_fails_when_interrupted_by_close(self): + """get_iter raises EOFError when close is called.""" + + def closer(): + time.sleep(2 * MS) + self.assembler.close() + + with self.run_in_thread(closer): + with self.assertRaises(EOFError): + list(self.assembler.get_iter()) + + def test_put_fails_when_interrupted_by_close(self): + """put raises EOFError when close is called.""" + + def closer(): + time.sleep(2 * MS) + self.assembler.close() + + with self.run_in_thread(closer): + with self.assertRaises(EOFError): + self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9")) + + def test_get_fails_after_close(self): + """get raises EOFError after close is called.""" + self.assembler.close() + with self.assertRaises(EOFError): + self.assembler.get() + + def test_get_iter_fails_after_close(self): + """get_iter raises EOFError after close is called.""" + self.assembler.close() + with self.assertRaises(EOFError): + list(self.assembler.get_iter()) + + def test_put_fails_after_close(self): + """put raises EOFError after close is called.""" + self.assembler.close() + with self.assertRaises(EOFError): + self.assembler.put(Frame(OP_TEXT, b"caf\xc3\xa9")) + + def test_close_is_idempotent(self): + """close can be called multiple times safely.""" + self.assembler.close() + self.assembler.close() diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/sync/test_server.py b/tests/wpt/tests/tools/third_party/websockets/tests/sync/test_server.py new file mode 100644 index 00000000000..f9db8424683 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/sync/test_server.py @@ -0,0 +1,388 @@ +import dataclasses +import http +import logging +import socket +import threading +import unittest + +from websockets.exceptions import ( + ConnectionClosedError, + ConnectionClosedOK, + InvalidStatus, + NegotiationError, +) +from websockets.http11 import Request, Response +from websockets.sync.server import * + +from ..utils import MS, temp_unix_socket_path +from .client import CLIENT_CONTEXT, run_client, run_unix_client +from .server import ( + SERVER_CONTEXT, + EvalShellMixin, + crash, + do_nothing, + eval_shell, + run_server, + run_unix_server, +) + + +class ServerTests(EvalShellMixin, unittest.TestCase): + def test_connection(self): + """Server receives connection from client and the handshake succeeds.""" + with run_server() as server: + with run_client(server) as client: + self.assertEval(client, "ws.protocol.state.name", "OPEN") + + def test_connection_fails(self): + """Server receives connection from client but the handshake fails.""" + + def remove_key_header(self, request): + del request.headers["Sec-WebSocket-Key"] + + with run_server(process_request=remove_key_header) as server: + with self.assertRaisesRegex( + InvalidStatus, + "server rejected WebSocket connection: HTTP 400", + ): + with run_client(server): + self.fail("did not raise") + + def test_connection_handler_returns(self): + """Connection handler returns.""" + with run_server(do_nothing) as server: + with run_client(server) as client: + with self.assertRaisesRegex( + ConnectionClosedOK, + r"received 1000 \(OK\); then sent 1000 \(OK\)", + ): + client.recv() + + def test_connection_handler_raises_exception(self): + """Connection handler raises an exception.""" + with run_server(crash) as server: + with run_client(server) as client: + with self.assertRaisesRegex( + ConnectionClosedError, + r"received 1011 \(internal error\); " + r"then sent 1011 \(internal error\)", + ): + client.recv() + + def test_existing_socket(self): + """Server receives connection using a pre-existing socket.""" + with socket.create_server(("localhost", 0)) as sock: + with run_server(sock=sock): + # Build WebSocket URI to ensure we connect to the right socket. + with run_client("ws://{}:{}/".format(*sock.getsockname())) as client: + self.assertEval(client, "ws.protocol.state.name", "OPEN") + + def test_select_subprotocol(self): + """Server selects a subprotocol with the select_subprotocol callable.""" + + def select_subprotocol(ws, subprotocols): + ws.select_subprotocol_ran = True + assert "chat" in subprotocols + return "chat" + + with run_server( + subprotocols=["chat"], + select_subprotocol=select_subprotocol, + ) as server: + with run_client(server, subprotocols=["chat"]) as client: + self.assertEval(client, "ws.select_subprotocol_ran", "True") + self.assertEval(client, "ws.subprotocol", "chat") + + def test_select_subprotocol_rejects_handshake(self): + """Server rejects handshake if select_subprotocol raises NegotiationError.""" + + def select_subprotocol(ws, subprotocols): + raise NegotiationError + + with run_server(select_subprotocol=select_subprotocol) as server: + with self.assertRaisesRegex( + InvalidStatus, + "server rejected WebSocket connection: HTTP 400", + ): + with run_client(server): + self.fail("did not raise") + + def test_select_subprotocol_raises_exception(self): + """Server returns an error if select_subprotocol raises an exception.""" + + def select_subprotocol(ws, subprotocols): + raise RuntimeError + + with run_server(select_subprotocol=select_subprotocol) as server: + with self.assertRaisesRegex( + InvalidStatus, + "server rejected WebSocket connection: HTTP 500", + ): + with run_client(server): + self.fail("did not raise") + + def test_process_request(self): + """Server runs process_request before processing the handshake.""" + + def process_request(ws, request): + self.assertIsInstance(request, Request) + ws.process_request_ran = True + + with run_server(process_request=process_request) as server: + with run_client(server) as client: + self.assertEval(client, "ws.process_request_ran", "True") + + def test_process_request_abort_handshake(self): + """Server aborts handshake if process_request returns a response.""" + + def process_request(ws, request): + return ws.protocol.reject(http.HTTPStatus.FORBIDDEN, "Forbidden") + + with run_server(process_request=process_request) as server: + with self.assertRaisesRegex( + InvalidStatus, + "server rejected WebSocket connection: HTTP 403", + ): + with run_client(server): + self.fail("did not raise") + + def test_process_request_raises_exception(self): + """Server returns an error if process_request raises an exception.""" + + def process_request(ws, request): + raise RuntimeError + + with run_server(process_request=process_request) as server: + with self.assertRaisesRegex( + InvalidStatus, + "server rejected WebSocket connection: HTTP 500", + ): + with run_client(server): + self.fail("did not raise") + + def test_process_response(self): + """Server runs process_response after processing the handshake.""" + + def process_response(ws, request, response): + self.assertIsInstance(request, Request) + self.assertIsInstance(response, Response) + ws.process_response_ran = True + + with run_server(process_response=process_response) as server: + with run_client(server) as client: + self.assertEval(client, "ws.process_response_ran", "True") + + def test_process_response_override_response(self): + """Server runs process_response after processing the handshake.""" + + def process_response(ws, request, response): + headers = response.headers.copy() + headers["X-ProcessResponse-Ran"] = "true" + return dataclasses.replace(response, headers=headers) + + with run_server(process_response=process_response) as server: + with run_client(server) as client: + self.assertEqual( + client.response.headers["X-ProcessResponse-Ran"], "true" + ) + + def test_process_response_raises_exception(self): + """Server returns an error if process_response raises an exception.""" + + def process_response(ws, request, response): + raise RuntimeError + + with run_server(process_response=process_response) as server: + with self.assertRaisesRegex( + InvalidStatus, + "server rejected WebSocket connection: HTTP 500", + ): + with run_client(server): + self.fail("did not raise") + + def test_override_server(self): + """Server can override Server header with server_header.""" + with run_server(server_header="Neo") as server: + with run_client(server) as client: + self.assertEval(client, "ws.response.headers['Server']", "Neo") + + def test_remove_server(self): + """Server can remove Server header with server_header.""" + with run_server(server_header=None) as server: + with run_client(server) as client: + self.assertEval(client, "'Server' in ws.response.headers", "False") + + def test_compression_is_enabled(self): + """Server enables compression by default.""" + with run_server() as server: + with run_client(server) as client: + self.assertEval( + client, + "[type(ext).__name__ for ext in ws.protocol.extensions]", + "['PerMessageDeflate']", + ) + + def test_disable_compression(self): + """Server disables compression.""" + with run_server(compression=None) as server: + with run_client(server) as client: + self.assertEval(client, "ws.protocol.extensions", "[]") + + def test_custom_connection_factory(self): + """Server runs ServerConnection factory provided in create_connection.""" + + def create_connection(*args, **kwargs): + server = ServerConnection(*args, **kwargs) + server.create_connection_ran = True + return server + + with run_server(create_connection=create_connection) as server: + with run_client(server) as client: + self.assertEval(client, "ws.create_connection_ran", "True") + + def test_timeout_during_handshake(self): + """Server times out before receiving handshake request from client.""" + with run_server(open_timeout=MS) as server: + with socket.create_connection(server.socket.getsockname()) as sock: + self.assertEqual(sock.recv(4096), b"") + + def test_connection_closed_during_handshake(self): + """Server reads EOF before receiving handshake request from client.""" + with run_server() as server: + # Patch handler to record a reference to the thread running it. + server_thread = None + conn_received = threading.Event() + original_handler = server.handler + + def handler(sock, addr): + nonlocal server_thread + server_thread = threading.current_thread() + nonlocal conn_received + conn_received.set() + original_handler(sock, addr) + + server.handler = handler + + with socket.create_connection(server.socket.getsockname()): + # Wait for the server to receive the connection, then close it. + conn_received.wait() + + # Wait for the server thread to terminate. + server_thread.join() + + +class SecureServerTests(EvalShellMixin, unittest.TestCase): + def test_connection(self): + """Server receives secure connection from client.""" + with run_server(ssl_context=SERVER_CONTEXT) as server: + with run_client(server, ssl_context=CLIENT_CONTEXT) as client: + self.assertEval(client, "ws.protocol.state.name", "OPEN") + self.assertEval(client, "ws.socket.version()[:3]", "TLS") + + def test_timeout_during_tls_handshake(self): + """Server times out before receiving TLS handshake request from client.""" + with run_server(ssl_context=SERVER_CONTEXT, open_timeout=MS) as server: + with socket.create_connection(server.socket.getsockname()) as sock: + self.assertEqual(sock.recv(4096), b"") + + def test_connection_closed_during_tls_handshake(self): + """Server reads EOF before receiving TLS handshake request from client.""" + with run_server(ssl_context=SERVER_CONTEXT) as server: + # Patch handler to record a reference to the thread running it. + server_thread = None + conn_received = threading.Event() + original_handler = server.handler + + def handler(sock, addr): + nonlocal server_thread + server_thread = threading.current_thread() + nonlocal conn_received + conn_received.set() + original_handler(sock, addr) + + server.handler = handler + + with socket.create_connection(server.socket.getsockname()): + # Wait for the server to receive the connection, then close it. + conn_received.wait() + + # Wait for the server thread to terminate. + server_thread.join() + + +@unittest.skipUnless(hasattr(socket, "AF_UNIX"), "this test requires Unix sockets") +class UnixServerTests(EvalShellMixin, unittest.TestCase): + def test_connection(self): + """Server receives connection from client over a Unix socket.""" + with temp_unix_socket_path() as path: + with run_unix_server(path): + with run_unix_client(path) as client: + self.assertEval(client, "ws.protocol.state.name", "OPEN") + + +@unittest.skipUnless(hasattr(socket, "AF_UNIX"), "this test requires Unix sockets") +class SecureUnixServerTests(EvalShellMixin, unittest.TestCase): + def test_connection(self): + """Server receives secure connection from client over a Unix socket.""" + with temp_unix_socket_path() as path: + with run_unix_server(path, ssl_context=SERVER_CONTEXT): + with run_unix_client(path, ssl_context=CLIENT_CONTEXT) as client: + self.assertEval(client, "ws.protocol.state.name", "OPEN") + self.assertEval(client, "ws.socket.version()[:3]", "TLS") + + +class ServerUsageErrorsTests(unittest.TestCase): + def test_unix_without_path_or_sock(self): + """Unix server requires path when sock isn't provided.""" + with self.assertRaisesRegex( + TypeError, + "missing path argument", + ): + unix_serve(eval_shell) + + def test_unix_with_path_and_sock(self): + """Unix server rejects path when sock is provided.""" + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.addCleanup(sock.close) + with self.assertRaisesRegex( + TypeError, + "path and sock arguments are incompatible", + ): + unix_serve(eval_shell, path="/", sock=sock) + + def test_invalid_subprotocol(self): + """Server rejects single value of subprotocols.""" + with self.assertRaisesRegex( + TypeError, + "subprotocols must be a list", + ): + serve(eval_shell, subprotocols="chat") + + def test_unsupported_compression(self): + """Server rejects incorrect value of compression.""" + with self.assertRaisesRegex( + ValueError, + "unsupported compression: False", + ): + serve(eval_shell, compression=False) + + +class WebSocketServerTests(unittest.TestCase): + def test_logger(self): + """WebSocketServer accepts a logger argument.""" + logger = logging.getLogger("test") + with run_server(logger=logger) as server: + self.assertIs(server.logger, logger) + + def test_fileno(self): + """WebSocketServer provides a fileno attribute.""" + with run_server() as server: + self.assertIsInstance(server.fileno(), int) + + def test_shutdown(self): + """WebSocketServer provides a shutdown method.""" + with run_server() as server: + server.shutdown() + # Check that the server socket is closed. + with self.assertRaises(OSError): + server.socket.accept() diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/sync/test_utils.py b/tests/wpt/tests/tools/third_party/websockets/tests/sync/test_utils.py new file mode 100644 index 00000000000..2980a97b428 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/sync/test_utils.py @@ -0,0 +1,33 @@ +import unittest + +from websockets.sync.utils import * + +from ..utils import MS + + +class DeadlineTests(unittest.TestCase): + def test_timeout_pending(self): + """timeout returns remaining time if deadline is in the future.""" + deadline = Deadline(MS) + timeout = deadline.timeout() + self.assertGreater(timeout, 0) + self.assertLess(timeout, MS) + + def test_timeout_elapsed_exception(self): + """timeout raises TimeoutError if deadline is in the past.""" + deadline = Deadline(-MS) + with self.assertRaises(TimeoutError): + deadline.timeout() + + def test_timeout_elapsed_no_exception(self): + """timeout doesn't raise TimeoutError when raise_if_elapsed is disabled.""" + deadline = Deadline(-MS) + timeout = deadline.timeout(raise_if_elapsed=False) + self.assertGreater(timeout, -2 * MS) + self.assertLess(timeout, -MS) + + def test_no_timeout(self): + """timeout returns None when no deadline is set.""" + deadline = Deadline(None) + timeout = deadline.timeout() + self.assertIsNone(timeout, None) diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/sync/utils.py b/tests/wpt/tests/tools/third_party/websockets/tests/sync/utils.py new file mode 100644 index 00000000000..8903cd34992 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/sync/utils.py @@ -0,0 +1,26 @@ +import contextlib +import threading +import time +import unittest + +from ..utils import MS + + +class ThreadTestCase(unittest.TestCase): + @contextlib.contextmanager + def run_in_thread(self, target): + """ + Run ``target`` function without arguments in a thread. + + In order to facilitate writing tests, this helper lets the thread run + for 1ms on entry and joins the thread with a 1ms timeout on exit. + + """ + thread = threading.Thread(target=target) + thread.start() + time.sleep(MS) + try: + yield + finally: + thread.join(MS) + self.assertFalse(thread.is_alive()) diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/test_auth.py b/tests/wpt/tests/tools/third_party/websockets/tests/test_auth.py new file mode 100644 index 00000000000..28db9315521 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/test_auth.py @@ -0,0 +1 @@ +from websockets.auth import * diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/test_client.py b/tests/wpt/tests/tools/third_party/websockets/tests/test_client.py new file mode 100644 index 00000000000..c83c87038f6 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/test_client.py @@ -0,0 +1,614 @@ +import logging +import unittest +import unittest.mock + +from websockets.client import * +from websockets.datastructures import Headers +from websockets.exceptions import InvalidHandshake, InvalidHeader +from websockets.frames import OP_TEXT, Frame +from websockets.http11 import Request, Response +from websockets.protocol import CONNECTING, OPEN +from websockets.uri import parse_uri +from websockets.utils import accept_key + +from .extensions.utils import ( + ClientOpExtensionFactory, + ClientRsv2ExtensionFactory, + OpExtension, + Rsv2Extension, +) +from .test_utils import ACCEPT, KEY +from .utils import DATE, DeprecationTestCase + + +class ConnectTests(unittest.TestCase): + def test_send_connect(self): + with unittest.mock.patch("websockets.client.generate_key", return_value=KEY): + client = ClientProtocol(parse_uri("wss://example.com/test")) + request = client.connect() + self.assertIsInstance(request, Request) + client.send_request(request) + self.assertEqual( + client.data_to_send(), + [ + f"GET /test HTTP/1.1\r\n" + f"Host: example.com\r\n" + f"Upgrade: websocket\r\n" + f"Connection: Upgrade\r\n" + f"Sec-WebSocket-Key: {KEY}\r\n" + f"Sec-WebSocket-Version: 13\r\n" + f"\r\n".encode() + ], + ) + self.assertFalse(client.close_expected()) + + def test_connect_request(self): + with unittest.mock.patch("websockets.client.generate_key", return_value=KEY): + client = ClientProtocol(parse_uri("wss://example.com/test")) + request = client.connect() + self.assertEqual(request.path, "/test") + self.assertEqual( + request.headers, + Headers( + { + "Host": "example.com", + "Upgrade": "websocket", + "Connection": "Upgrade", + "Sec-WebSocket-Key": KEY, + "Sec-WebSocket-Version": "13", + } + ), + ) + + def test_path(self): + client = ClientProtocol(parse_uri("wss://example.com/endpoint?test=1")) + request = client.connect() + + self.assertEqual(request.path, "/endpoint?test=1") + + def test_port(self): + for uri, host in [ + ("ws://example.com/", "example.com"), + ("ws://example.com:80/", "example.com"), + ("ws://example.com:8080/", "example.com:8080"), + ("wss://example.com/", "example.com"), + ("wss://example.com:443/", "example.com"), + ("wss://example.com:8443/", "example.com:8443"), + ]: + with self.subTest(uri=uri): + client = ClientProtocol(parse_uri(uri)) + request = client.connect() + + self.assertEqual(request.headers["Host"], host) + + def test_user_info(self): + client = ClientProtocol(parse_uri("wss://hello:iloveyou@example.com/")) + request = client.connect() + + self.assertEqual(request.headers["Authorization"], "Basic aGVsbG86aWxvdmV5b3U=") + + def test_origin(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + origin="https://example.com", + ) + request = client.connect() + + self.assertEqual(request.headers["Origin"], "https://example.com") + + def test_extensions(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + extensions=[ClientOpExtensionFactory()], + ) + request = client.connect() + + self.assertEqual(request.headers["Sec-WebSocket-Extensions"], "x-op; op") + + def test_subprotocols(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + subprotocols=["chat"], + ) + request = client.connect() + + self.assertEqual(request.headers["Sec-WebSocket-Protocol"], "chat") + + +class AcceptRejectTests(unittest.TestCase): + def test_receive_accept(self): + with unittest.mock.patch("websockets.client.generate_key", return_value=KEY): + client = ClientProtocol(parse_uri("ws://example.com/test")) + client.connect() + client.receive_data( + ( + f"HTTP/1.1 101 Switching Protocols\r\n" + f"Upgrade: websocket\r\n" + f"Connection: Upgrade\r\n" + f"Sec-WebSocket-Accept: {ACCEPT}\r\n" + f"Date: {DATE}\r\n" + f"\r\n" + ).encode(), + ) + [response] = client.events_received() + self.assertIsInstance(response, Response) + self.assertEqual(client.data_to_send(), []) + self.assertFalse(client.close_expected()) + self.assertEqual(client.state, OPEN) + + def test_receive_reject(self): + with unittest.mock.patch("websockets.client.generate_key", return_value=KEY): + client = ClientProtocol(parse_uri("ws://example.com/test")) + client.connect() + client.receive_data( + ( + f"HTTP/1.1 404 Not Found\r\n" + f"Date: {DATE}\r\n" + f"Content-Length: 13\r\n" + f"Content-Type: text/plain; charset=utf-8\r\n" + f"Connection: close\r\n" + f"\r\n" + f"Sorry folks.\n" + ).encode(), + ) + [response] = client.events_received() + self.assertIsInstance(response, Response) + self.assertEqual(client.data_to_send(), []) + self.assertTrue(client.close_expected()) + self.assertEqual(client.state, CONNECTING) + + def test_accept_response(self): + with unittest.mock.patch("websockets.client.generate_key", return_value=KEY): + client = ClientProtocol(parse_uri("ws://example.com/test")) + client.connect() + client.receive_data( + ( + f"HTTP/1.1 101 Switching Protocols\r\n" + f"Upgrade: websocket\r\n" + f"Connection: Upgrade\r\n" + f"Sec-WebSocket-Accept: {ACCEPT}\r\n" + f"Date: {DATE}\r\n" + f"\r\n" + ).encode(), + ) + [response] = client.events_received() + self.assertEqual(response.status_code, 101) + self.assertEqual(response.reason_phrase, "Switching Protocols") + self.assertEqual( + response.headers, + Headers( + { + "Upgrade": "websocket", + "Connection": "Upgrade", + "Sec-WebSocket-Accept": ACCEPT, + "Date": DATE, + } + ), + ) + self.assertIsNone(response.body) + + def test_reject_response(self): + with unittest.mock.patch("websockets.client.generate_key", return_value=KEY): + client = ClientProtocol(parse_uri("ws://example.com/test")) + client.connect() + client.receive_data( + ( + f"HTTP/1.1 404 Not Found\r\n" + f"Date: {DATE}\r\n" + f"Content-Length: 13\r\n" + f"Content-Type: text/plain; charset=utf-8\r\n" + f"Connection: close\r\n" + f"\r\n" + f"Sorry folks.\n" + ).encode(), + ) + [response] = client.events_received() + self.assertEqual(response.status_code, 404) + self.assertEqual(response.reason_phrase, "Not Found") + self.assertEqual( + response.headers, + Headers( + { + "Date": DATE, + "Content-Length": "13", + "Content-Type": "text/plain; charset=utf-8", + "Connection": "close", + } + ), + ) + self.assertEqual(response.body, b"Sorry folks.\n") + + def test_no_response(self): + with unittest.mock.patch("websockets.client.generate_key", return_value=KEY): + client = ClientProtocol(parse_uri("ws://example.com/test")) + client.connect() + client.receive_eof() + self.assertEqual(client.events_received(), []) + + def test_partial_response(self): + with unittest.mock.patch("websockets.client.generate_key", return_value=KEY): + client = ClientProtocol(parse_uri("ws://example.com/test")) + client.connect() + client.receive_data(b"HTTP/1.1 101 Switching Protocols\r\n") + client.receive_eof() + self.assertEqual(client.events_received(), []) + + def test_random_response(self): + with unittest.mock.patch("websockets.client.generate_key", return_value=KEY): + client = ClientProtocol(parse_uri("ws://example.com/test")) + client.connect() + client.receive_data(b"220 smtp.invalid\r\n") + client.receive_data(b"250 Hello relay.invalid\r\n") + client.receive_data(b"250 Ok\r\n") + client.receive_data(b"250 Ok\r\n") + client.receive_eof() + self.assertEqual(client.events_received(), []) + + def make_accept_response(self, client): + request = client.connect() + return Response( + status_code=101, + reason_phrase="Switching Protocols", + headers=Headers( + { + "Upgrade": "websocket", + "Connection": "Upgrade", + "Sec-WebSocket-Accept": accept_key( + request.headers["Sec-WebSocket-Key"] + ), + } + ), + ) + + def test_basic(self): + client = ClientProtocol(parse_uri("wss://example.com/")) + response = self.make_accept_response(client) + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, OPEN) + + def test_missing_connection(self): + client = ClientProtocol(parse_uri("wss://example.com/")) + response = self.make_accept_response(client) + del response.headers["Connection"] + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHeader) as raised: + raise client.handshake_exc + self.assertEqual(str(raised.exception), "missing Connection header") + + def test_invalid_connection(self): + client = ClientProtocol(parse_uri("wss://example.com/")) + response = self.make_accept_response(client) + del response.headers["Connection"] + response.headers["Connection"] = "close" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHeader) as raised: + raise client.handshake_exc + self.assertEqual(str(raised.exception), "invalid Connection header: close") + + def test_missing_upgrade(self): + client = ClientProtocol(parse_uri("wss://example.com/")) + response = self.make_accept_response(client) + del response.headers["Upgrade"] + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHeader) as raised: + raise client.handshake_exc + self.assertEqual(str(raised.exception), "missing Upgrade header") + + def test_invalid_upgrade(self): + client = ClientProtocol(parse_uri("wss://example.com/")) + response = self.make_accept_response(client) + del response.headers["Upgrade"] + response.headers["Upgrade"] = "h2c" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHeader) as raised: + raise client.handshake_exc + self.assertEqual(str(raised.exception), "invalid Upgrade header: h2c") + + def test_missing_accept(self): + client = ClientProtocol(parse_uri("wss://example.com/")) + response = self.make_accept_response(client) + del response.headers["Sec-WebSocket-Accept"] + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHeader) as raised: + raise client.handshake_exc + self.assertEqual(str(raised.exception), "missing Sec-WebSocket-Accept header") + + def test_multiple_accept(self): + client = ClientProtocol(parse_uri("wss://example.com/")) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Accept"] = ACCEPT + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHeader) as raised: + raise client.handshake_exc + self.assertEqual( + str(raised.exception), + "invalid Sec-WebSocket-Accept header: " + "more than one Sec-WebSocket-Accept header found", + ) + + def test_invalid_accept(self): + client = ClientProtocol(parse_uri("wss://example.com/")) + response = self.make_accept_response(client) + del response.headers["Sec-WebSocket-Accept"] + response.headers["Sec-WebSocket-Accept"] = ACCEPT + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHeader) as raised: + raise client.handshake_exc + self.assertEqual( + str(raised.exception), f"invalid Sec-WebSocket-Accept header: {ACCEPT}" + ) + + def test_no_extensions(self): + client = ClientProtocol(parse_uri("wss://example.com/")) + response = self.make_accept_response(client) + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, OPEN) + self.assertEqual(client.extensions, []) + + def test_no_extension(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + extensions=[ClientOpExtensionFactory()], + ) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Extensions"] = "x-op; op" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, OPEN) + self.assertEqual(client.extensions, [OpExtension()]) + + def test_extension(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + extensions=[ClientRsv2ExtensionFactory()], + ) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Extensions"] = "x-rsv2" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, OPEN) + self.assertEqual(client.extensions, [Rsv2Extension()]) + + def test_unexpected_extension(self): + client = ClientProtocol(parse_uri("wss://example.com/")) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Extensions"] = "x-op; op" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHandshake) as raised: + raise client.handshake_exc + self.assertEqual(str(raised.exception), "no extensions supported") + + def test_unsupported_extension(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + extensions=[ClientRsv2ExtensionFactory()], + ) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Extensions"] = "x-op; op" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHandshake) as raised: + raise client.handshake_exc + self.assertEqual( + str(raised.exception), + "Unsupported extension: name = x-op, params = [('op', None)]", + ) + + def test_supported_extension_parameters(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + extensions=[ClientOpExtensionFactory("this")], + ) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Extensions"] = "x-op; op=this" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, OPEN) + self.assertEqual(client.extensions, [OpExtension("this")]) + + def test_unsupported_extension_parameters(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + extensions=[ClientOpExtensionFactory("this")], + ) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Extensions"] = "x-op; op=that" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHandshake) as raised: + raise client.handshake_exc + self.assertEqual( + str(raised.exception), + "Unsupported extension: name = x-op, params = [('op', 'that')]", + ) + + def test_multiple_supported_extension_parameters(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + extensions=[ + ClientOpExtensionFactory("this"), + ClientOpExtensionFactory("that"), + ], + ) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Extensions"] = "x-op; op=that" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, OPEN) + self.assertEqual(client.extensions, [OpExtension("that")]) + + def test_multiple_extensions(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + extensions=[ClientOpExtensionFactory(), ClientRsv2ExtensionFactory()], + ) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Extensions"] = "x-op; op" + response.headers["Sec-WebSocket-Extensions"] = "x-rsv2" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, OPEN) + self.assertEqual(client.extensions, [OpExtension(), Rsv2Extension()]) + + def test_multiple_extensions_order(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + extensions=[ClientOpExtensionFactory(), ClientRsv2ExtensionFactory()], + ) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Extensions"] = "x-rsv2" + response.headers["Sec-WebSocket-Extensions"] = "x-op; op" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, OPEN) + self.assertEqual(client.extensions, [Rsv2Extension(), OpExtension()]) + + def test_no_subprotocols(self): + client = ClientProtocol(parse_uri("wss://example.com/")) + response = self.make_accept_response(client) + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, OPEN) + self.assertIsNone(client.subprotocol) + + def test_no_subprotocol(self): + client = ClientProtocol(parse_uri("wss://example.com/"), subprotocols=["chat"]) + response = self.make_accept_response(client) + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, OPEN) + self.assertIsNone(client.subprotocol) + + def test_subprotocol(self): + client = ClientProtocol(parse_uri("wss://example.com/"), subprotocols=["chat"]) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Protocol"] = "chat" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, OPEN) + self.assertEqual(client.subprotocol, "chat") + + def test_unexpected_subprotocol(self): + client = ClientProtocol(parse_uri("wss://example.com/")) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Protocol"] = "chat" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHandshake) as raised: + raise client.handshake_exc + self.assertEqual(str(raised.exception), "no subprotocols supported") + + def test_multiple_subprotocols(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + subprotocols=["superchat", "chat"], + ) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Protocol"] = "superchat" + response.headers["Sec-WebSocket-Protocol"] = "chat" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHandshake) as raised: + raise client.handshake_exc + self.assertEqual( + str(raised.exception), "multiple subprotocols: superchat, chat" + ) + + def test_supported_subprotocol(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + subprotocols=["superchat", "chat"], + ) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Protocol"] = "chat" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, OPEN) + self.assertEqual(client.subprotocol, "chat") + + def test_unsupported_subprotocol(self): + client = ClientProtocol( + parse_uri("wss://example.com/"), + subprotocols=["superchat", "chat"], + ) + response = self.make_accept_response(client) + response.headers["Sec-WebSocket-Protocol"] = "otherchat" + client.receive_data(response.serialize()) + [response] = client.events_received() + + self.assertEqual(client.state, CONNECTING) + with self.assertRaises(InvalidHandshake) as raised: + raise client.handshake_exc + self.assertEqual(str(raised.exception), "unsupported subprotocol: otherchat") + + +class MiscTests(unittest.TestCase): + def test_bypass_handshake(self): + client = ClientProtocol(parse_uri("ws://example.com/test"), state=OPEN) + client.receive_data(b"\x81\x06Hello!") + [frame] = client.events_received() + self.assertEqual(frame, Frame(OP_TEXT, b"Hello!")) + + def test_custom_logger(self): + logger = logging.getLogger("test") + with self.assertLogs("test", logging.DEBUG) as logs: + ClientProtocol(parse_uri("wss://example.com/test"), logger=logger) + self.assertEqual(len(logs.records), 1) + + +class BackwardsCompatibilityTests(DeprecationTestCase): + def test_client_connection_class(self): + with self.assertDeprecationWarning( + "ClientConnection was renamed to ClientProtocol" + ): + from websockets.client import ClientConnection + + client = ClientConnection("ws://localhost/") + + self.assertIsInstance(client, ClientProtocol) diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/test_connection.py b/tests/wpt/tests/tools/third_party/websockets/tests/test_connection.py new file mode 100644 index 00000000000..6592d67d0d4 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/test_connection.py @@ -0,0 +1,14 @@ +from websockets.protocol import Protocol + +from .utils import DeprecationTestCase + + +class BackwardsCompatibilityTests(DeprecationTestCase): + def test_connection_class(self): + with self.assertDeprecationWarning( + "websockets.connection was renamed to websockets.protocol " + "and Connection was renamed to Protocol" + ): + from websockets.connection import Connection + + self.assertIs(Connection, Protocol) diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/test_datastructures.py b/tests/wpt/tests/tools/third_party/websockets/tests/test_datastructures.py new file mode 100644 index 00000000000..32b79817ae7 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/test_datastructures.py @@ -0,0 +1,236 @@ +import unittest + +from websockets.datastructures import * + + +class MultipleValuesErrorTests(unittest.TestCase): + def test_multiple_values_error_str(self): + self.assertEqual(str(MultipleValuesError("Connection")), "'Connection'") + self.assertEqual(str(MultipleValuesError()), "") + + +class HeadersTests(unittest.TestCase): + def setUp(self): + self.headers = Headers([("Connection", "Upgrade"), ("Server", "websockets")]) + + def test_init(self): + self.assertEqual( + Headers(), + Headers(), + ) + + def test_init_from_kwargs(self): + self.assertEqual( + Headers(connection="Upgrade", server="websockets"), + self.headers, + ) + + def test_init_from_headers(self): + self.assertEqual( + Headers(self.headers), + self.headers, + ) + + def test_init_from_headers_and_kwargs(self): + self.assertEqual( + Headers(Headers(connection="Upgrade"), server="websockets"), + self.headers, + ) + + def test_init_from_mapping(self): + self.assertEqual( + Headers({"Connection": "Upgrade", "Server": "websockets"}), + self.headers, + ) + + def test_init_from_mapping_and_kwargs(self): + self.assertEqual( + Headers({"Connection": "Upgrade"}, server="websockets"), + self.headers, + ) + + def test_init_from_iterable(self): + self.assertEqual( + Headers([("Connection", "Upgrade"), ("Server", "websockets")]), + self.headers, + ) + + def test_init_from_iterable_and_kwargs(self): + self.assertEqual( + Headers([("Connection", "Upgrade")], server="websockets"), + self.headers, + ) + + def test_init_multiple_positional_arguments(self): + with self.assertRaises(TypeError): + Headers(Headers(connection="Upgrade"), Headers(server="websockets")) + + def test_str(self): + self.assertEqual( + str(self.headers), "Connection: Upgrade\r\nServer: websockets\r\n\r\n" + ) + + def test_repr(self): + self.assertEqual( + repr(self.headers), + "Headers([('Connection', 'Upgrade'), ('Server', 'websockets')])", + ) + + def test_copy(self): + self.assertEqual(repr(self.headers.copy()), repr(self.headers)) + + def test_serialize(self): + self.assertEqual( + self.headers.serialize(), + b"Connection: Upgrade\r\nServer: websockets\r\n\r\n", + ) + + def test_contains(self): + self.assertIn("Server", self.headers) + + def test_contains_case_insensitive(self): + self.assertIn("server", self.headers) + + def test_contains_not_found(self): + self.assertNotIn("Date", self.headers) + + def test_contains_non_string_key(self): + self.assertNotIn(42, self.headers) + + def test_iter(self): + self.assertEqual(set(iter(self.headers)), {"connection", "server"}) + + def test_len(self): + self.assertEqual(len(self.headers), 2) + + def test_getitem(self): + self.assertEqual(self.headers["Server"], "websockets") + + def test_getitem_case_insensitive(self): + self.assertEqual(self.headers["server"], "websockets") + + def test_getitem_key_error(self): + with self.assertRaises(KeyError): + self.headers["Upgrade"] + + def test_setitem(self): + self.headers["Upgrade"] = "websocket" + self.assertEqual(self.headers["Upgrade"], "websocket") + + def test_setitem_case_insensitive(self): + self.headers["upgrade"] = "websocket" + self.assertEqual(self.headers["Upgrade"], "websocket") + + def test_delitem(self): + del self.headers["Connection"] + with self.assertRaises(KeyError): + self.headers["Connection"] + + def test_delitem_case_insensitive(self): + del self.headers["connection"] + with self.assertRaises(KeyError): + self.headers["Connection"] + + def test_eq(self): + other_headers = Headers([("Connection", "Upgrade"), ("Server", "websockets")]) + self.assertEqual(self.headers, other_headers) + + def test_eq_case_insensitive(self): + other_headers = Headers(connection="Upgrade", server="websockets") + self.assertEqual(self.headers, other_headers) + + def test_eq_not_equal(self): + other_headers = Headers([("Connection", "close"), ("Server", "websockets")]) + self.assertNotEqual(self.headers, other_headers) + + def test_eq_other_type(self): + self.assertNotEqual( + self.headers, "Connection: Upgrade\r\nServer: websockets\r\n\r\n" + ) + + def test_clear(self): + self.headers.clear() + self.assertFalse(self.headers) + self.assertEqual(self.headers, Headers()) + + def test_get_all(self): + self.assertEqual(self.headers.get_all("Connection"), ["Upgrade"]) + + def test_get_all_case_insensitive(self): + self.assertEqual(self.headers.get_all("connection"), ["Upgrade"]) + + def test_get_all_no_values(self): + self.assertEqual(self.headers.get_all("Upgrade"), []) + + def test_raw_items(self): + self.assertEqual( + list(self.headers.raw_items()), + [("Connection", "Upgrade"), ("Server", "websockets")], + ) + + +class MultiValueHeadersTests(unittest.TestCase): + def setUp(self): + self.headers = Headers([("Server", "Python"), ("Server", "websockets")]) + + def test_init_from_headers(self): + self.assertEqual( + Headers(self.headers), + self.headers, + ) + + def test_init_from_headers_and_kwargs(self): + self.assertEqual( + Headers(Headers(server="Python"), server="websockets"), + self.headers, + ) + + def test_str(self): + self.assertEqual( + str(self.headers), "Server: Python\r\nServer: websockets\r\n\r\n" + ) + + def test_repr(self): + self.assertEqual( + repr(self.headers), + "Headers([('Server', 'Python'), ('Server', 'websockets')])", + ) + + def test_copy(self): + self.assertEqual(repr(self.headers.copy()), repr(self.headers)) + + def test_serialize(self): + self.assertEqual( + self.headers.serialize(), + b"Server: Python\r\nServer: websockets\r\n\r\n", + ) + + def test_iter(self): + self.assertEqual(set(iter(self.headers)), {"server"}) + + def test_len(self): + self.assertEqual(len(self.headers), 1) + + def test_getitem_multiple_values_error(self): + with self.assertRaises(MultipleValuesError): + self.headers["Server"] + + def test_setitem(self): + self.headers["Server"] = "redux" + self.assertEqual( + self.headers.get_all("Server"), ["Python", "websockets", "redux"] + ) + + def test_delitem(self): + del self.headers["Server"] + with self.assertRaises(KeyError): + self.headers["Server"] + + def test_get_all(self): + self.assertEqual(self.headers.get_all("Server"), ["Python", "websockets"]) + + def test_raw_items(self): + self.assertEqual( + list(self.headers.raw_items()), + [("Server", "Python"), ("Server", "websockets")], + ) diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/test_exceptions.py b/tests/wpt/tests/tools/third_party/websockets/tests/test_exceptions.py new file mode 100644 index 00000000000..1e6f58fad58 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/test_exceptions.py @@ -0,0 +1,196 @@ +import unittest + +from websockets.datastructures import Headers +from websockets.exceptions import * +from websockets.frames import Close, CloseCode +from websockets.http11 import Response + + +class ExceptionsTests(unittest.TestCase): + def test_str(self): + for exception, exception_str in [ + ( + WebSocketException("something went wrong"), + "something went wrong", + ), + ( + ConnectionClosed( + Close(CloseCode.NORMAL_CLOSURE, ""), + Close(CloseCode.NORMAL_CLOSURE, ""), + True, + ), + "received 1000 (OK); then sent 1000 (OK)", + ), + ( + ConnectionClosed( + Close(CloseCode.GOING_AWAY, "Bye!"), + Close(CloseCode.GOING_AWAY, "Bye!"), + False, + ), + "sent 1001 (going away) Bye!; then received 1001 (going away) Bye!", + ), + ( + ConnectionClosed( + Close(CloseCode.NORMAL_CLOSURE, "race"), + Close(CloseCode.NORMAL_CLOSURE, "cond"), + True, + ), + "received 1000 (OK) race; then sent 1000 (OK) cond", + ), + ( + ConnectionClosed( + Close(CloseCode.NORMAL_CLOSURE, "cond"), + Close(CloseCode.NORMAL_CLOSURE, "race"), + False, + ), + "sent 1000 (OK) race; then received 1000 (OK) cond", + ), + ( + ConnectionClosed( + None, + Close(CloseCode.MESSAGE_TOO_BIG, ""), + None, + ), + "sent 1009 (message too big); no close frame received", + ), + ( + ConnectionClosed( + Close(CloseCode.PROTOCOL_ERROR, ""), + None, + None, + ), + "received 1002 (protocol error); no close frame sent", + ), + ( + ConnectionClosedOK( + Close(CloseCode.NORMAL_CLOSURE, ""), + Close(CloseCode.NORMAL_CLOSURE, ""), + True, + ), + "received 1000 (OK); then sent 1000 (OK)", + ), + ( + ConnectionClosedError( + None, + None, + None, + ), + "no close frame received or sent", + ), + ( + InvalidHandshake("invalid request"), + "invalid request", + ), + ( + SecurityError("redirect from WSS to WS"), + "redirect from WSS to WS", + ), + ( + InvalidMessage("malformed HTTP message"), + "malformed HTTP message", + ), + ( + InvalidHeader("Name"), + "missing Name header", + ), + ( + InvalidHeader("Name", None), + "missing Name header", + ), + ( + InvalidHeader("Name", ""), + "empty Name header", + ), + ( + InvalidHeader("Name", "Value"), + "invalid Name header: Value", + ), + ( + InvalidHeaderFormat("Sec-WebSocket-Protocol", "exp. token", "a=|", 3), + "invalid Sec-WebSocket-Protocol header: exp. token at 3 in a=|", + ), + ( + InvalidHeaderValue("Sec-WebSocket-Version", "42"), + "invalid Sec-WebSocket-Version header: 42", + ), + ( + InvalidOrigin("http://bad.origin"), + "invalid Origin header: http://bad.origin", + ), + ( + InvalidUpgrade("Upgrade"), + "missing Upgrade header", + ), + ( + InvalidUpgrade("Connection", "websocket"), + "invalid Connection header: websocket", + ), + ( + InvalidStatus(Response(401, "Unauthorized", Headers())), + "server rejected WebSocket connection: HTTP 401", + ), + ( + InvalidStatusCode(403, Headers()), + "server rejected WebSocket connection: HTTP 403", + ), + ( + NegotiationError("unsupported subprotocol: spam"), + "unsupported subprotocol: spam", + ), + ( + DuplicateParameter("a"), + "duplicate parameter: a", + ), + ( + InvalidParameterName("|"), + "invalid parameter name: |", + ), + ( + InvalidParameterValue("a", None), + "missing value for parameter a", + ), + ( + InvalidParameterValue("a", ""), + "empty value for parameter a", + ), + ( + InvalidParameterValue("a", "|"), + "invalid value for parameter a: |", + ), + ( + AbortHandshake(200, Headers(), b"OK\n"), + "HTTP 200, 0 headers, 3 bytes", + ), + ( + RedirectHandshake("wss://example.com"), + "redirect to wss://example.com", + ), + ( + InvalidState("WebSocket connection isn't established yet"), + "WebSocket connection isn't established yet", + ), + ( + InvalidURI("|", "not at all!"), + "| isn't a valid URI: not at all!", + ), + ( + PayloadTooBig("payload length exceeds limit: 2 > 1 bytes"), + "payload length exceeds limit: 2 > 1 bytes", + ), + ( + ProtocolError("invalid opcode: 7"), + "invalid opcode: 7", + ), + ]: + with self.subTest(exception=exception): + self.assertEqual(str(exception), exception_str) + + def test_connection_closed_attributes_backwards_compatibility(self): + exception = ConnectionClosed(Close(CloseCode.NORMAL_CLOSURE, "OK"), None, None) + self.assertEqual(exception.code, CloseCode.NORMAL_CLOSURE) + self.assertEqual(exception.reason, "OK") + + def test_connection_closed_attributes_backwards_compatibility_defaults(self): + exception = ConnectionClosed(None, None, None) + self.assertEqual(exception.code, CloseCode.ABNORMAL_CLOSURE) + self.assertEqual(exception.reason, "") diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/test_exports.py b/tests/wpt/tests/tools/third_party/websockets/tests/test_exports.py new file mode 100644 index 00000000000..67a1a6f994f --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/test_exports.py @@ -0,0 +1,30 @@ +import unittest + +import websockets +import websockets.auth +import websockets.client +import websockets.datastructures +import websockets.exceptions +import websockets.legacy.protocol +import websockets.server +import websockets.typing +import websockets.uri + + +combined_exports = ( + websockets.auth.__all__ + + websockets.client.__all__ + + websockets.datastructures.__all__ + + websockets.exceptions.__all__ + + websockets.legacy.protocol.__all__ + + websockets.server.__all__ + + websockets.typing.__all__ +) + + +class ExportsTests(unittest.TestCase): + def test_top_level_module_reexports_all_submodule_exports(self): + self.assertEqual(set(combined_exports), set(websockets.__all__)) + + def test_submodule_exports_are_globally_unique(self): + self.assertEqual(len(set(combined_exports)), len(combined_exports)) diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/test_frames.py b/tests/wpt/tests/tools/third_party/websockets/tests/test_frames.py new file mode 100644 index 00000000000..e323b3b57c8 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/test_frames.py @@ -0,0 +1,495 @@ +import codecs +import dataclasses +import unittest +import unittest.mock + +from websockets.exceptions import PayloadTooBig, ProtocolError +from websockets.frames import * +from websockets.frames import CloseCode +from websockets.streams import StreamReader + +from .utils import GeneratorTestCase + + +class FramesTestCase(GeneratorTestCase): + def enforce_mask(self, mask): + return unittest.mock.patch("secrets.token_bytes", return_value=mask) + + def parse(self, data, mask, max_size=None, extensions=None): + """ + Parse a frame from a bytestring. + + """ + reader = StreamReader() + reader.feed_data(data) + reader.feed_eof() + parser = Frame.parse( + reader.read_exact, mask=mask, max_size=max_size, extensions=extensions + ) + return self.assertGeneratorReturns(parser) + + def assertFrameData(self, frame, data, mask, extensions=None): + """ + Serializing frame yields data. Parsing data yields frame. + + """ + # Compare frames first, because test failures are easier to read, + # especially when mask = True. + parsed = self.parse(data, mask=mask, extensions=extensions) + self.assertEqual(parsed, frame) + + # Make masking deterministic by reusing the same "random" mask. + # This has an effect only when mask is True. + mask_bytes = data[2:6] if mask else b"" + with self.enforce_mask(mask_bytes): + serialized = frame.serialize(mask=mask, extensions=extensions) + self.assertEqual(serialized, data) + + +class FrameTests(FramesTestCase): + def test_text_unmasked(self): + self.assertFrameData( + Frame(OP_TEXT, b"Spam"), + b"\x81\x04Spam", + mask=False, + ) + + def test_text_masked(self): + self.assertFrameData( + Frame(OP_TEXT, b"Spam"), + b"\x81\x84\x5b\xfb\xe1\xa8\x08\x8b\x80\xc5", + mask=True, + ) + + def test_binary_unmasked(self): + self.assertFrameData( + Frame(OP_BINARY, b"Eggs"), + b"\x82\x04Eggs", + mask=False, + ) + + def test_binary_masked(self): + self.assertFrameData( + Frame(OP_BINARY, b"Eggs"), + b"\x82\x84\x53\xcd\xe2\x89\x16\xaa\x85\xfa", + mask=True, + ) + + def test_non_ascii_text_unmasked(self): + self.assertFrameData( + Frame(OP_TEXT, "café".encode("utf-8")), + b"\x81\x05caf\xc3\xa9", + mask=False, + ) + + def test_non_ascii_text_masked(self): + self.assertFrameData( + Frame(OP_TEXT, "café".encode("utf-8")), + b"\x81\x85\x64\xbe\xee\x7e\x07\xdf\x88\xbd\xcd", + mask=True, + ) + + def test_close(self): + self.assertFrameData( + Frame(OP_CLOSE, b""), + b"\x88\x00", + mask=False, + ) + + def test_ping(self): + self.assertFrameData( + Frame(OP_PING, b"ping"), + b"\x89\x04ping", + mask=False, + ) + + def test_pong(self): + self.assertFrameData( + Frame(OP_PONG, b"pong"), + b"\x8a\x04pong", + mask=False, + ) + + def test_long(self): + self.assertFrameData( + Frame(OP_BINARY, 126 * b"a"), + b"\x82\x7e\x00\x7e" + 126 * b"a", + mask=False, + ) + + def test_very_long(self): + self.assertFrameData( + Frame(OP_BINARY, 65536 * b"a"), + b"\x82\x7f\x00\x00\x00\x00\x00\x01\x00\x00" + 65536 * b"a", + mask=False, + ) + + def test_payload_too_big(self): + with self.assertRaises(PayloadTooBig): + self.parse(b"\x82\x7e\x04\x01" + 1025 * b"a", mask=False, max_size=1024) + + def test_bad_reserved_bits(self): + for data in [b"\xc0\x00", b"\xa0\x00", b"\x90\x00"]: + with self.subTest(data=data): + with self.assertRaises(ProtocolError): + self.parse(data, mask=False) + + def test_good_opcode(self): + for opcode in list(range(0x00, 0x03)) + list(range(0x08, 0x0B)): + data = bytes([0x80 | opcode, 0]) + with self.subTest(data=data): + self.parse(data, mask=False) # does not raise an exception + + def test_bad_opcode(self): + for opcode in list(range(0x03, 0x08)) + list(range(0x0B, 0x10)): + data = bytes([0x80 | opcode, 0]) + with self.subTest(data=data): + with self.assertRaises(ProtocolError): + self.parse(data, mask=False) + + def test_mask_flag(self): + # Mask flag correctly set. + self.parse(b"\x80\x80\x00\x00\x00\x00", mask=True) + # Mask flag incorrectly unset. + with self.assertRaises(ProtocolError): + self.parse(b"\x80\x80\x00\x00\x00\x00", mask=False) + # Mask flag correctly unset. + self.parse(b"\x80\x00", mask=False) + # Mask flag incorrectly set. + with self.assertRaises(ProtocolError): + self.parse(b"\x80\x00", mask=True) + + def test_control_frame_max_length(self): + # At maximum allowed length. + self.parse(b"\x88\x7e\x00\x7d" + 125 * b"a", mask=False) + # Above maximum allowed length. + with self.assertRaises(ProtocolError): + self.parse(b"\x88\x7e\x00\x7e" + 126 * b"a", mask=False) + + def test_fragmented_control_frame(self): + # Fin bit correctly set. + self.parse(b"\x88\x00", mask=False) + # Fin bit incorrectly unset. + with self.assertRaises(ProtocolError): + self.parse(b"\x08\x00", mask=False) + + def test_extensions(self): + class Rot13: + @staticmethod + def encode(frame): + assert frame.opcode == OP_TEXT + text = frame.data.decode() + data = codecs.encode(text, "rot13").encode() + return dataclasses.replace(frame, data=data) + + # This extensions is symmetrical. + @staticmethod + def decode(frame, *, max_size=None): + return Rot13.encode(frame) + + self.assertFrameData( + Frame(OP_TEXT, b"hello"), + b"\x81\x05uryyb", + mask=False, + extensions=[Rot13()], + ) + + +class StrTests(unittest.TestCase): + def test_cont_text(self): + self.assertEqual( + str(Frame(OP_CONT, b" cr\xc3\xa8me", fin=False)), + "CONT ' crème' [text, 7 bytes, continued]", + ) + + def test_cont_binary(self): + self.assertEqual( + str(Frame(OP_CONT, b"\xfc\xfd\xfe\xff", fin=False)), + "CONT fc fd fe ff [binary, 4 bytes, continued]", + ) + + def test_cont_binary_from_memoryview(self): + self.assertEqual( + str(Frame(OP_CONT, memoryview(b"\xfc\xfd\xfe\xff"), fin=False)), + "CONT fc fd fe ff [binary, 4 bytes, continued]", + ) + + def test_cont_final_text(self): + self.assertEqual( + str(Frame(OP_CONT, b" cr\xc3\xa8me")), + "CONT ' crème' [text, 7 bytes]", + ) + + def test_cont_final_binary(self): + self.assertEqual( + str(Frame(OP_CONT, b"\xfc\xfd\xfe\xff")), + "CONT fc fd fe ff [binary, 4 bytes]", + ) + + def test_cont_final_binary_from_memoryview(self): + self.assertEqual( + str(Frame(OP_CONT, memoryview(b"\xfc\xfd\xfe\xff"))), + "CONT fc fd fe ff [binary, 4 bytes]", + ) + + def test_cont_text_truncated(self): + self.assertEqual( + str(Frame(OP_CONT, b"caf\xc3\xa9 " * 16, fin=False)), + "CONT 'café café café café café café café café café ca..." + "fé café café café café ' [text, 96 bytes, continued]", + ) + + def test_cont_binary_truncated(self): + self.assertEqual( + str(Frame(OP_CONT, bytes(range(256)), fin=False)), + "CONT 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ..." + " f8 f9 fa fb fc fd fe ff [binary, 256 bytes, continued]", + ) + + def test_cont_binary_truncated_from_memoryview(self): + self.assertEqual( + str(Frame(OP_CONT, memoryview(bytes(range(256))), fin=False)), + "CONT 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ..." + " f8 f9 fa fb fc fd fe ff [binary, 256 bytes, continued]", + ) + + def test_text(self): + self.assertEqual( + str(Frame(OP_TEXT, b"caf\xc3\xa9")), + "TEXT 'café' [5 bytes]", + ) + + def test_text_non_final(self): + self.assertEqual( + str(Frame(OP_TEXT, b"caf\xc3\xa9", fin=False)), + "TEXT 'café' [5 bytes, continued]", + ) + + def test_text_truncated(self): + self.assertEqual( + str(Frame(OP_TEXT, b"caf\xc3\xa9 " * 16)), + "TEXT 'café café café café café café café café café ca..." + "fé café café café café ' [96 bytes]", + ) + + def test_text_with_newline(self): + self.assertEqual( + str(Frame(OP_TEXT, b"Hello\nworld!")), + "TEXT 'Hello\\nworld!' [12 bytes]", + ) + + def test_binary(self): + self.assertEqual( + str(Frame(OP_BINARY, b"\x00\x01\x02\x03")), + "BINARY 00 01 02 03 [4 bytes]", + ) + + def test_binary_from_memoryview(self): + self.assertEqual( + str(Frame(OP_BINARY, memoryview(b"\x00\x01\x02\x03"))), + "BINARY 00 01 02 03 [4 bytes]", + ) + + def test_binary_non_final(self): + self.assertEqual( + str(Frame(OP_BINARY, b"\x00\x01\x02\x03", fin=False)), + "BINARY 00 01 02 03 [4 bytes, continued]", + ) + + def test_binary_non_final_from_memoryview(self): + self.assertEqual( + str(Frame(OP_BINARY, memoryview(b"\x00\x01\x02\x03"), fin=False)), + "BINARY 00 01 02 03 [4 bytes, continued]", + ) + + def test_binary_truncated(self): + self.assertEqual( + str(Frame(OP_BINARY, bytes(range(256)))), + "BINARY 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ..." + " f8 f9 fa fb fc fd fe ff [256 bytes]", + ) + + def test_binary_truncated_from_memoryview(self): + self.assertEqual( + str(Frame(OP_BINARY, memoryview(bytes(range(256))))), + "BINARY 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f ..." + " f8 f9 fa fb fc fd fe ff [256 bytes]", + ) + + def test_close(self): + self.assertEqual( + str(Frame(OP_CLOSE, b"\x03\xe8")), + "CLOSE 1000 (OK) [2 bytes]", + ) + + def test_close_reason(self): + self.assertEqual( + str(Frame(OP_CLOSE, b"\x03\xe9Bye!")), + "CLOSE 1001 (going away) Bye! [6 bytes]", + ) + + def test_ping(self): + self.assertEqual( + str(Frame(OP_PING, b"")), + "PING '' [0 bytes]", + ) + + def test_ping_text(self): + self.assertEqual( + str(Frame(OP_PING, b"ping")), + "PING 'ping' [text, 4 bytes]", + ) + + def test_ping_text_with_newline(self): + self.assertEqual( + str(Frame(OP_PING, b"ping\n")), + "PING 'ping\\n' [text, 5 bytes]", + ) + + def test_ping_binary(self): + self.assertEqual( + str(Frame(OP_PING, b"\xff\x00\xff\x00")), + "PING ff 00 ff 00 [binary, 4 bytes]", + ) + + def test_pong(self): + self.assertEqual( + str(Frame(OP_PONG, b"")), + "PONG '' [0 bytes]", + ) + + def test_pong_text(self): + self.assertEqual( + str(Frame(OP_PONG, b"pong")), + "PONG 'pong' [text, 4 bytes]", + ) + + def test_pong_text_with_newline(self): + self.assertEqual( + str(Frame(OP_PONG, b"pong\n")), + "PONG 'pong\\n' [text, 5 bytes]", + ) + + def test_pong_binary(self): + self.assertEqual( + str(Frame(OP_PONG, b"\xff\x00\xff\x00")), + "PONG ff 00 ff 00 [binary, 4 bytes]", + ) + + +class PrepareDataTests(unittest.TestCase): + def test_prepare_data_str(self): + self.assertEqual( + prepare_data("café"), + (OP_TEXT, b"caf\xc3\xa9"), + ) + + def test_prepare_data_bytes(self): + self.assertEqual( + prepare_data(b"tea"), + (OP_BINARY, b"tea"), + ) + + def test_prepare_data_bytearray(self): + self.assertEqual( + prepare_data(bytearray(b"tea")), + (OP_BINARY, bytearray(b"tea")), + ) + + def test_prepare_data_memoryview(self): + self.assertEqual( + prepare_data(memoryview(b"tea")), + (OP_BINARY, memoryview(b"tea")), + ) + + def test_prepare_data_list(self): + with self.assertRaises(TypeError): + prepare_data([]) + + def test_prepare_data_none(self): + with self.assertRaises(TypeError): + prepare_data(None) + + +class PrepareCtrlTests(unittest.TestCase): + def test_prepare_ctrl_str(self): + self.assertEqual(prepare_ctrl("café"), b"caf\xc3\xa9") + + def test_prepare_ctrl_bytes(self): + self.assertEqual(prepare_ctrl(b"tea"), b"tea") + + def test_prepare_ctrl_bytearray(self): + self.assertEqual(prepare_ctrl(bytearray(b"tea")), b"tea") + + def test_prepare_ctrl_memoryview(self): + self.assertEqual(prepare_ctrl(memoryview(b"tea")), b"tea") + + def test_prepare_ctrl_list(self): + with self.assertRaises(TypeError): + prepare_ctrl([]) + + def test_prepare_ctrl_none(self): + with self.assertRaises(TypeError): + prepare_ctrl(None) + + +class CloseTests(unittest.TestCase): + def assertCloseData(self, close, data): + """ + Serializing close yields data. Parsing data yields close. + + """ + serialized = close.serialize() + self.assertEqual(serialized, data) + parsed = Close.parse(data) + self.assertEqual(parsed, close) + + def test_str(self): + self.assertEqual( + str(Close(CloseCode.NORMAL_CLOSURE, "")), + "1000 (OK)", + ) + self.assertEqual( + str(Close(CloseCode.GOING_AWAY, "Bye!")), + "1001 (going away) Bye!", + ) + self.assertEqual( + str(Close(3000, "")), + "3000 (registered)", + ) + self.assertEqual( + str(Close(4000, "")), + "4000 (private use)", + ) + self.assertEqual( + str(Close(5000, "")), + "5000 (unknown)", + ) + + def test_parse_and_serialize(self): + self.assertCloseData( + Close(CloseCode.NORMAL_CLOSURE, "OK"), + b"\x03\xe8OK", + ) + self.assertCloseData( + Close(CloseCode.GOING_AWAY, ""), + b"\x03\xe9", + ) + + def test_parse_empty(self): + self.assertEqual( + Close.parse(b""), + Close(CloseCode.NO_STATUS_RCVD, ""), + ) + + def test_parse_errors(self): + with self.assertRaises(ProtocolError): + Close.parse(b"\x03") + with self.assertRaises(ProtocolError): + Close.parse(b"\x03\xe7") + with self.assertRaises(UnicodeDecodeError): + Close.parse(b"\x03\xe8\xff\xff") + + def test_serialize_errors(self): + with self.assertRaises(ProtocolError): + Close(999, "").serialize() diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/test_headers.py b/tests/wpt/tests/tools/third_party/websockets/tests/test_headers.py new file mode 100644 index 00000000000..4ebd8b90cfe --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/test_headers.py @@ -0,0 +1,222 @@ +import unittest + +from websockets.exceptions import InvalidHeaderFormat, InvalidHeaderValue +from websockets.headers import * + + +class HeadersTests(unittest.TestCase): + def test_build_host(self): + for (host, port, secure), result in [ + (("localhost", 80, False), "localhost"), + (("localhost", 8000, False), "localhost:8000"), + (("localhost", 443, True), "localhost"), + (("localhost", 8443, True), "localhost:8443"), + (("example.com", 80, False), "example.com"), + (("example.com", 8000, False), "example.com:8000"), + (("example.com", 443, True), "example.com"), + (("example.com", 8443, True), "example.com:8443"), + (("127.0.0.1", 80, False), "127.0.0.1"), + (("127.0.0.1", 8000, False), "127.0.0.1:8000"), + (("127.0.0.1", 443, True), "127.0.0.1"), + (("127.0.0.1", 8443, True), "127.0.0.1:8443"), + (("::1", 80, False), "[::1]"), + (("::1", 8000, False), "[::1]:8000"), + (("::1", 443, True), "[::1]"), + (("::1", 8443, True), "[::1]:8443"), + ]: + with self.subTest(host=host, port=port, secure=secure): + self.assertEqual(build_host(host, port, secure), result) + + def test_parse_connection(self): + for header, parsed in [ + # Realistic use cases + ("Upgrade", ["Upgrade"]), # Safari, Chrome + ("keep-alive, Upgrade", ["keep-alive", "Upgrade"]), # Firefox + # Pathological example + (",,\t, , ,Upgrade ,,", ["Upgrade"]), + ]: + with self.subTest(header=header): + self.assertEqual(parse_connection(header), parsed) + + def test_parse_connection_invalid_header_format(self): + for header in ["???", "keep-alive; Upgrade"]: + with self.subTest(header=header): + with self.assertRaises(InvalidHeaderFormat): + parse_connection(header) + + def test_parse_upgrade(self): + for header, parsed in [ + # Realistic use case + ("websocket", ["websocket"]), + # Synthetic example + ("http/3.0, websocket", ["http/3.0", "websocket"]), + # Pathological example + (",, WebSocket, \t,,", ["WebSocket"]), + ]: + with self.subTest(header=header): + self.assertEqual(parse_upgrade(header), parsed) + + def test_parse_upgrade_invalid_header_format(self): + for header in ["???", "websocket 2", "http/3.0; websocket"]: + with self.subTest(header=header): + with self.assertRaises(InvalidHeaderFormat): + parse_upgrade(header) + + def test_parse_extension(self): + for header, parsed in [ + # Synthetic examples + ("foo", [("foo", [])]), + ("foo, bar", [("foo", []), ("bar", [])]), + ( + 'foo; name; token=token; quoted-string="quoted-string", ' + "bar; quux; quuux", + [ + ( + "foo", + [ + ("name", None), + ("token", "token"), + ("quoted-string", "quoted-string"), + ], + ), + ("bar", [("quux", None), ("quuux", None)]), + ], + ), + # Pathological example + ( + ",\t, , ,foo ;bar = 42,, baz,,", + [("foo", [("bar", "42")]), ("baz", [])], + ), + # Realistic use cases for permessage-deflate + ("permessage-deflate", [("permessage-deflate", [])]), + ( + "permessage-deflate; client_max_window_bits", + [("permessage-deflate", [("client_max_window_bits", None)])], + ), + ( + "permessage-deflate; server_max_window_bits=10", + [("permessage-deflate", [("server_max_window_bits", "10")])], + ), + ]: + with self.subTest(header=header): + self.assertEqual(parse_extension(header), parsed) + # Also ensure that build_extension round-trips cleanly. + unparsed = build_extension(parsed) + self.assertEqual(parse_extension(unparsed), parsed) + + def test_parse_extension_invalid_header_format(self): + for header in [ + # Truncated examples + "", + ",\t,", + "foo;", + "foo; bar;", + "foo; bar=", + 'foo; bar="baz', + # Wrong delimiter + "foo, bar, baz=quux; quuux", + # Value in quoted string parameter that isn't a token + 'foo; bar=" "', + ]: + with self.subTest(header=header): + with self.assertRaises(InvalidHeaderFormat): + parse_extension(header) + + def test_parse_subprotocol(self): + for header, parsed in [ + # Synthetic examples + ("foo", ["foo"]), + ("foo, bar", ["foo", "bar"]), + # Pathological example + (",\t, , ,foo ,, bar,baz,,", ["foo", "bar", "baz"]), + ]: + with self.subTest(header=header): + self.assertEqual(parse_subprotocol(header), parsed) + # Also ensure that build_subprotocol round-trips cleanly. + unparsed = build_subprotocol(parsed) + self.assertEqual(parse_subprotocol(unparsed), parsed) + + def test_parse_subprotocol_invalid_header(self): + for header in [ + # Truncated examples + "", + ",\t,", + # Wrong delimiter + "foo; bar", + ]: + with self.subTest(header=header): + with self.assertRaises(InvalidHeaderFormat): + parse_subprotocol(header) + + def test_validate_subprotocols(self): + for subprotocols in [[], ["sip"], ["v1.usp"], ["sip", "v1.usp"]]: + with self.subTest(subprotocols=subprotocols): + validate_subprotocols(subprotocols) + + def test_validate_subprotocols_invalid(self): + for subprotocols, exception in [ + ({"sip": None}, TypeError), + ("sip", TypeError), + ([""], ValueError), + ]: + with self.subTest(subprotocols=subprotocols): + with self.assertRaises(exception): + validate_subprotocols(subprotocols) + + def test_build_www_authenticate_basic(self): + # Test vector from RFC 7617 + self.assertEqual( + build_www_authenticate_basic("foo"), 'Basic realm="foo", charset="UTF-8"' + ) + + def test_build_www_authenticate_basic_invalid_realm(self): + # Realm contains a control character forbidden in quoted-string encoding + with self.assertRaises(ValueError): + build_www_authenticate_basic("\u0007") + + def test_build_authorization_basic(self): + # Test vector from RFC 7617 + self.assertEqual( + build_authorization_basic("Aladdin", "open sesame"), + "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", + ) + + def test_build_authorization_basic_utf8(self): + # Test vector from RFC 7617 + self.assertEqual( + build_authorization_basic("test", "123£"), "Basic dGVzdDoxMjPCow==" + ) + + def test_parse_authorization_basic(self): + for header, parsed in [ + ("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", ("Aladdin", "open sesame")), + # Password contains non-ASCII character + ("Basic dGVzdDoxMjPCow==", ("test", "123£")), + # Password contains a colon + ("Basic YWxhZGRpbjpvcGVuOnNlc2FtZQ==", ("aladdin", "open:sesame")), + # Scheme name must be case insensitive + ("basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", ("Aladdin", "open sesame")), + ]: + with self.subTest(header=header): + self.assertEqual(parse_authorization_basic(header), parsed) + + def test_parse_authorization_basic_invalid_header_format(self): + for header in [ + "// Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", + "Basic\tQWxhZGRpbjpvcGVuIHNlc2FtZQ==", + "Basic ****************************", + "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== //", + ]: + with self.subTest(header=header): + with self.assertRaises(InvalidHeaderFormat): + parse_authorization_basic(header) + + def test_parse_authorization_basic_invalid_header_value(self): + for header in [ + "Digest ...", + "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ", + "Basic QWxhZGNlc2FtZQ==", + ]: + with self.subTest(header=header): + with self.assertRaises(InvalidHeaderValue): + parse_authorization_basic(header) diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/test_http.py b/tests/wpt/tests/tools/third_party/websockets/tests/test_http.py new file mode 100644 index 00000000000..036bc14102b --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/test_http.py @@ -0,0 +1 @@ +from websockets.http import * diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/test_http11.py b/tests/wpt/tests/tools/third_party/websockets/tests/test_http11.py new file mode 100644 index 00000000000..d2e5e04627c --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/test_http11.py @@ -0,0 +1,344 @@ +from websockets.datastructures import Headers +from websockets.exceptions import SecurityError +from websockets.http11 import * +from websockets.http11 import parse_headers +from websockets.streams import StreamReader + +from .utils import GeneratorTestCase + + +class RequestTests(GeneratorTestCase): + def setUp(self): + super().setUp() + self.reader = StreamReader() + + def parse(self): + return Request.parse(self.reader.read_line) + + def test_parse(self): + # Example from the protocol overview in RFC 6455 + self.reader.feed_data( + b"GET /chat HTTP/1.1\r\n" + b"Host: server.example.com\r\n" + b"Upgrade: websocket\r\n" + b"Connection: Upgrade\r\n" + b"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" + b"Origin: http://example.com\r\n" + b"Sec-WebSocket-Protocol: chat, superchat\r\n" + b"Sec-WebSocket-Version: 13\r\n" + b"\r\n" + ) + request = self.assertGeneratorReturns(self.parse()) + self.assertEqual(request.path, "/chat") + self.assertEqual(request.headers["Upgrade"], "websocket") + + def test_parse_empty(self): + self.reader.feed_eof() + with self.assertRaises(EOFError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "connection closed while reading HTTP request line", + ) + + def test_parse_invalid_request_line(self): + self.reader.feed_data(b"GET /\r\n\r\n") + with self.assertRaises(ValueError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "invalid HTTP request line: GET /", + ) + + def test_parse_unsupported_method(self): + self.reader.feed_data(b"OPTIONS * HTTP/1.1\r\n\r\n") + with self.assertRaises(ValueError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "unsupported HTTP method: OPTIONS", + ) + + def test_parse_unsupported_version(self): + self.reader.feed_data(b"GET /chat HTTP/1.0\r\n\r\n") + with self.assertRaises(ValueError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "unsupported HTTP version: HTTP/1.0", + ) + + def test_parse_invalid_header(self): + self.reader.feed_data(b"GET /chat HTTP/1.1\r\nOops\r\n") + with self.assertRaises(ValueError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "invalid HTTP header line: Oops", + ) + + def test_parse_body(self): + self.reader.feed_data(b"GET / HTTP/1.1\r\nContent-Length: 3\r\n\r\nYo\n") + with self.assertRaises(ValueError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "unsupported request body", + ) + + def test_parse_body_with_transfer_encoding(self): + self.reader.feed_data(b"GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n") + with self.assertRaises(NotImplementedError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "transfer codings aren't supported", + ) + + def test_serialize(self): + # Example from the protocol overview in RFC 6455 + request = Request( + "/chat", + Headers( + [ + ("Host", "server.example.com"), + ("Upgrade", "websocket"), + ("Connection", "Upgrade"), + ("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ=="), + ("Origin", "http://example.com"), + ("Sec-WebSocket-Protocol", "chat, superchat"), + ("Sec-WebSocket-Version", "13"), + ] + ), + ) + self.assertEqual( + request.serialize(), + b"GET /chat HTTP/1.1\r\n" + b"Host: server.example.com\r\n" + b"Upgrade: websocket\r\n" + b"Connection: Upgrade\r\n" + b"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" + b"Origin: http://example.com\r\n" + b"Sec-WebSocket-Protocol: chat, superchat\r\n" + b"Sec-WebSocket-Version: 13\r\n" + b"\r\n", + ) + + +class ResponseTests(GeneratorTestCase): + def setUp(self): + super().setUp() + self.reader = StreamReader() + + def parse(self): + return Response.parse( + self.reader.read_line, + self.reader.read_exact, + self.reader.read_to_eof, + ) + + def test_parse(self): + # Example from the protocol overview in RFC 6455 + self.reader.feed_data( + b"HTTP/1.1 101 Switching Protocols\r\n" + b"Upgrade: websocket\r\n" + b"Connection: Upgrade\r\n" + b"Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" + b"Sec-WebSocket-Protocol: chat\r\n" + b"\r\n" + ) + response = self.assertGeneratorReturns(self.parse()) + self.assertEqual(response.status_code, 101) + self.assertEqual(response.reason_phrase, "Switching Protocols") + self.assertEqual(response.headers["Upgrade"], "websocket") + self.assertIsNone(response.body) + + def test_parse_empty(self): + self.reader.feed_eof() + with self.assertRaises(EOFError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "connection closed while reading HTTP status line", + ) + + def test_parse_invalid_status_line(self): + self.reader.feed_data(b"Hello!\r\n") + with self.assertRaises(ValueError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "invalid HTTP status line: Hello!", + ) + + def test_parse_unsupported_version(self): + self.reader.feed_data(b"HTTP/1.0 400 Bad Request\r\n\r\n") + with self.assertRaises(ValueError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "unsupported HTTP version: HTTP/1.0", + ) + + def test_parse_invalid_status(self): + self.reader.feed_data(b"HTTP/1.1 OMG WTF\r\n\r\n") + with self.assertRaises(ValueError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "invalid HTTP status code: OMG", + ) + + def test_parse_unsupported_status(self): + self.reader.feed_data(b"HTTP/1.1 007 My name is Bond\r\n\r\n") + with self.assertRaises(ValueError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "unsupported HTTP status code: 007", + ) + + def test_parse_invalid_reason(self): + self.reader.feed_data(b"HTTP/1.1 200 \x7f\r\n\r\n") + with self.assertRaises(ValueError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "invalid HTTP reason phrase: \x7f", + ) + + def test_parse_invalid_header(self): + self.reader.feed_data(b"HTTP/1.1 500 Internal Server Error\r\nOops\r\n") + with self.assertRaises(ValueError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "invalid HTTP header line: Oops", + ) + + def test_parse_body_with_content_length(self): + self.reader.feed_data( + b"HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello world!\n" + ) + response = self.assertGeneratorReturns(self.parse()) + self.assertEqual(response.body, b"Hello world!\n") + + def test_parse_body_without_content_length(self): + self.reader.feed_data(b"HTTP/1.1 200 OK\r\n\r\nHello world!\n") + gen = self.parse() + self.assertGeneratorRunning(gen) + self.reader.feed_eof() + response = self.assertGeneratorReturns(gen) + self.assertEqual(response.body, b"Hello world!\n") + + def test_parse_body_with_content_length_too_long(self): + self.reader.feed_data(b"HTTP/1.1 200 OK\r\nContent-Length: 1048577\r\n\r\n") + with self.assertRaises(SecurityError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "body too large: 1048577 bytes", + ) + + def test_parse_body_without_content_length_too_long(self): + self.reader.feed_data(b"HTTP/1.1 200 OK\r\n\r\n" + b"a" * 1048577) + with self.assertRaises(SecurityError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "body too large: over 1048576 bytes", + ) + + def test_parse_body_with_transfer_encoding(self): + self.reader.feed_data(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n") + with self.assertRaises(NotImplementedError) as raised: + next(self.parse()) + self.assertEqual( + str(raised.exception), + "transfer codings aren't supported", + ) + + def test_parse_body_no_content(self): + self.reader.feed_data(b"HTTP/1.1 204 No Content\r\n\r\n") + response = self.assertGeneratorReturns(self.parse()) + self.assertIsNone(response.body) + + def test_parse_body_not_modified(self): + self.reader.feed_data(b"HTTP/1.1 304 Not Modified\r\n\r\n") + response = self.assertGeneratorReturns(self.parse()) + self.assertIsNone(response.body) + + def test_serialize(self): + # Example from the protocol overview in RFC 6455 + response = Response( + 101, + "Switching Protocols", + Headers( + [ + ("Upgrade", "websocket"), + ("Connection", "Upgrade"), + ("Sec-WebSocket-Accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="), + ("Sec-WebSocket-Protocol", "chat"), + ] + ), + ) + self.assertEqual( + response.serialize(), + b"HTTP/1.1 101 Switching Protocols\r\n" + b"Upgrade: websocket\r\n" + b"Connection: Upgrade\r\n" + b"Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n" + b"Sec-WebSocket-Protocol: chat\r\n" + b"\r\n", + ) + + def test_serialize_with_body(self): + response = Response( + 200, + "OK", + Headers([("Content-Length", "13"), ("Content-Type", "text/plain")]), + b"Hello world!\n", + ) + self.assertEqual( + response.serialize(), + b"HTTP/1.1 200 OK\r\n" + b"Content-Length: 13\r\n" + b"Content-Type: text/plain\r\n" + b"\r\n" + b"Hello world!\n", + ) + + +class HeadersTests(GeneratorTestCase): + def setUp(self): + super().setUp() + self.reader = StreamReader() + + def parse_headers(self): + return parse_headers(self.reader.read_line) + + def test_parse_invalid_name(self): + self.reader.feed_data(b"foo bar: baz qux\r\n\r\n") + with self.assertRaises(ValueError): + next(self.parse_headers()) + + def test_parse_invalid_value(self): + self.reader.feed_data(b"foo: \x00\x00\x0f\r\n\r\n") + with self.assertRaises(ValueError): + next(self.parse_headers()) + + def test_parse_too_long_value(self): + self.reader.feed_data(b"foo: bar\r\n" * 129 + b"\r\n") + with self.assertRaises(SecurityError): + next(self.parse_headers()) + + def test_parse_too_long_line(self): + # Header line contains 5 + 8186 + 2 = 8193 bytes. + self.reader.feed_data(b"foo: " + b"a" * 8186 + b"\r\n\r\n") + with self.assertRaises(SecurityError): + next(self.parse_headers()) + + def test_parse_invalid_line_ending(self): + self.reader.feed_data(b"foo: bar\n\n") + with self.assertRaises(EOFError): + next(self.parse_headers()) diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/test_imports.py b/tests/wpt/tests/tools/third_party/websockets/tests/test_imports.py new file mode 100644 index 00000000000..b69ed931626 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/test_imports.py @@ -0,0 +1,64 @@ +import types +import unittest +import warnings + +from websockets.imports import * + + +foo = object() + +bar = object() + + +class ImportsTests(unittest.TestCase): + def setUp(self): + self.mod = types.ModuleType("tests.test_imports.test_alias") + self.mod.__package__ = self.mod.__name__ + + def test_get_alias(self): + lazy_import( + vars(self.mod), + aliases={"foo": "...test_imports"}, + ) + + self.assertEqual(self.mod.foo, foo) + + def test_get_deprecated_alias(self): + lazy_import( + vars(self.mod), + deprecated_aliases={"bar": "...test_imports"}, + ) + + with warnings.catch_warnings(record=True) as recorded_warnings: + warnings.simplefilter("always") + self.assertEqual(self.mod.bar, bar) + + self.assertEqual(len(recorded_warnings), 1) + warning = recorded_warnings[0].message + self.assertEqual( + str(warning), "tests.test_imports.test_alias.bar is deprecated" + ) + self.assertEqual(type(warning), DeprecationWarning) + + def test_dir(self): + lazy_import( + vars(self.mod), + aliases={"foo": "...test_imports"}, + deprecated_aliases={"bar": "...test_imports"}, + ) + + self.assertEqual( + [item for item in dir(self.mod) if not item[:2] == item[-2:] == "__"], + ["bar", "foo"], + ) + + def test_attribute_error(self): + lazy_import(vars(self.mod)) + + with self.assertRaises(AttributeError) as raised: + self.mod.foo + + self.assertEqual( + str(raised.exception), + "module 'tests.test_imports.test_alias' has no attribute 'foo'", + ) diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/test_localhost.cnf b/tests/wpt/tests/tools/third_party/websockets/tests/test_localhost.cnf new file mode 100644 index 00000000000..4069e39670c --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/test_localhost.cnf @@ -0,0 +1,27 @@ +[ req ] + +default_md = sha256 +encrypt_key = no + +prompt = no + +distinguished_name = dn +x509_extensions = ext + +[ dn ] + +C = "FR" +L = "Paris" +O = "Aymeric Augustin" +CN = "localhost" + +[ ext ] + +subjectAltName = @san + +[ san ] + +DNS.1 = localhost +DNS.2 = overridden +IP.3 = 127.0.0.1 +IP.4 = ::1 diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/test_localhost.pem b/tests/wpt/tests/tools/third_party/websockets/tests/test_localhost.pem new file mode 100644 index 00000000000..8df63ec8f4b --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/test_localhost.pem @@ -0,0 +1,48 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDYOOQyq8yYtn5x +K3yRborFxTFse16JIVb4x/ZhZgGm49eARCi09fmczQxJdQpHz81Ij6z0xi7AUYH7 +9wS8T0Lh3uGFDDS1GzITUVPIqSUi0xim2T6XPzXFVQYI1D/OjUxlHm+3/up+WwbL +sBgBO/lDmzoa3ZN7kt9HQoGc/14oQz1Qsv1QTDQs69r+o7mmBJr/hf/g7S0Csyy3 +iC6aaq+yCUyzDbjXceTI7WJqbTGNnK0/DjdFD/SJS/uSDNEg0AH53eqcCSjm+Ei/ +UF8qR5Pu4sSsNwToOW2MVgjtHFazc+kG3rzD6+3Dp+t6x6uI/npyuudOMCmOtd6z +kX0UPQaNAgMBAAECggEAS4eMBztGC+5rusKTEAZKSY15l0h9HG/d/qdzJFDKsO6T +/8VPZu8pk6F48kwFHFK1hexSYWq9OAcA3fBK4jDZzybZJm2+F6l5U5AsMUMMqt6M +lPP8Tj8RXG433muuIkvvbL82DVLpvNu1Qv+vUvcNOpWFtY7DDv6eKjlMJ3h4/pzh +89MNt26VMCYOlq1NSjuZBzFohL2u9nsFehlOpcVsqNfNfcYCq9+5yoH8fWJP90Op +hqhvqUoGLN7DRKV1f+AWHSA4nmGgvVviV5PQgMhtk5exlN7kG+rDc3LbzhefS1Sp +Tat1qIgm8fK2n+Q/obQPjHOGOGuvE5cIF7E275ZKgQKBgQDt87BqALKWnbkbQnb7 +GS1h6LRcKyZhFbxnO2qbviBWSo15LEF8jPGV33Dj+T56hqufa/rUkbZiUbIR9yOX +dnOwpAVTo+ObAwZfGfHvrnufiIbHFqJBumaYLqjRZ7AC0QtS3G+kjS9dbllrr7ok +fO4JdfKRXzBJKrkQdCn8hR22rQKBgQDon0b49Dxs1EfdSDbDode2TSwE83fI3vmR +SKUkNY8ma6CRbomVRWijhBM458wJeuhpjPZOvjNMsnDzGwrtdAp2VfFlMIDnA8ZC +fEWIAAH2QYKXKGmkoXOcWB2QbvbI154zCm6zFGtzvRKOCGmTXuhFajO8VPwOyJVt +aSJA3bLrYQKBgQDJM2/tAfAAKRdW9GlUwqI8Ep9G+/l0yANJqtTnIemH7XwYhJJO +9YJlPszfB2aMBgliQNSUHy1/jyKpzDYdITyLlPUoFwEilnkxuud2yiuf5rpH51yF +hU6wyWtXvXv3tbkEdH42PmdZcjBMPQeBSN2hxEi6ISncBDL9tau26PwJ9QKBgQCs +cNYl2reoXTzgtpWSNDk6NL769JjJWTFcF6QD0YhKjOI8rNpkw00sWc3+EybXqDr9 +c7dq6+gPZQAB1vwkxi6zRkZqIqiLl+qygnjwtkC+EhYCg7y8g8q2DUPtO7TJcb0e +TQ9+xRZad8B3dZj93A8G1hF//OfU9bB/qL3xo+bsQQKBgC/9YJvgLIWA/UziLcB2 +29Ai0nbPkN5df7z4PifUHHSlbQJHKak8UKbMP+8S064Ul0F7g8UCjZMk2LzSbaNY +XU5+2j0sIOnGUFoSlvcpdowzYrD2LN5PkKBot7AOq/v7HlcOoR8J8RGWAMpCrHsI +a/u/dlZs+/K16RcavQwx8rag +-----END PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDWTCCAkGgAwIBAgIJAOL9UKiOOxupMA0GCSqGSIb3DQEBCwUAMEwxCzAJBgNV +BAYTAkZSMQ4wDAYDVQQHDAVQYXJpczEZMBcGA1UECgwQQXltZXJpYyBBdWd1c3Rp +bjESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTIyMTAxNTE5Mjg0MVoYDzIwNjQxMDE0 +MTkyODQxWjBMMQswCQYDVQQGEwJGUjEOMAwGA1UEBwwFUGFyaXMxGTAXBgNVBAoM +EEF5bWVyaWMgQXVndXN0aW4xEjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANg45DKrzJi2fnErfJFuisXFMWx7XokhVvjH +9mFmAabj14BEKLT1+ZzNDEl1CkfPzUiPrPTGLsBRgfv3BLxPQuHe4YUMNLUbMhNR +U8ipJSLTGKbZPpc/NcVVBgjUP86NTGUeb7f+6n5bBsuwGAE7+UObOhrdk3uS30dC +gZz/XihDPVCy/VBMNCzr2v6juaYEmv+F/+DtLQKzLLeILppqr7IJTLMNuNdx5Mjt +YmptMY2crT8ON0UP9IlL+5IM0SDQAfnd6pwJKOb4SL9QXypHk+7ixKw3BOg5bYxW +CO0cVrNz6QbevMPr7cOn63rHq4j+enK6504wKY613rORfRQ9Bo0CAwEAAaM8MDow +OAYDVR0RBDEwL4IJbG9jYWxob3N0ggpvdmVycmlkZGVuhwR/AAABhxAAAAAAAAAA +AAAAAAAAAAABMA0GCSqGSIb3DQEBCwUAA4IBAQBPNDGDdl4wsCRlDuyCHBC8o+vW +Vb14thUw9Z6UrlsQRXLONxHOXbNAj1sYQACNwIWuNz36HXu5m8Xw/ID/bOhnIg+b +Y6l/JU/kZQYB7SV1aR3ZdbCK0gjfkE0POBHuKOjUFIOPBCtJ4tIBUX94zlgJrR9v +2rqJC3TIYrR7pVQumHZsI5GZEMpM5NxfreWwxcgltgxmGdm7elcizHfz7k5+szwh +4eZ/rxK9bw1q8BIvVBWelRvUR55mIrCjzfZp5ZObSYQTZlW7PzXBe5Jk+1w31YHM +RSBA2EpPhYlGNqPidi7bg7rnQcsc6+hE0OqzTL/hWxPm9Vbp9dj3HFTik1wa +-----END CERTIFICATE----- diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/test_protocol.py b/tests/wpt/tests/tools/third_party/websockets/tests/test_protocol.py new file mode 100644 index 00000000000..a64172b5398 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/test_protocol.py @@ -0,0 +1,1790 @@ +import logging +import unittest.mock + +from websockets.exceptions import ( + ConnectionClosedError, + ConnectionClosedOK, + InvalidState, + PayloadTooBig, + ProtocolError, +) +from websockets.frames import ( + OP_BINARY, + OP_CLOSE, + OP_CONT, + OP_PING, + OP_PONG, + OP_TEXT, + Close, + CloseCode, + Frame, +) +from websockets.protocol import * +from websockets.protocol import CLIENT, CLOSED, CLOSING, SERVER + +from .extensions.utils import Rsv2Extension +from .test_frames import FramesTestCase + + +class ProtocolTestCase(FramesTestCase): + def assertFrameSent(self, connection, frame, eof=False): + """ + Outgoing data for ``connection`` contains the given frame. + + ``frame`` may be ``None`` if no frame is expected. + + When ``eof`` is ``True``, the end of the stream is also expected. + + """ + frames_sent = [ + None + if write is SEND_EOF + else self.parse( + write, + mask=connection.side is CLIENT, + extensions=connection.extensions, + ) + for write in connection.data_to_send() + ] + frames_expected = [] if frame is None else [frame] + if eof: + frames_expected += [None] + self.assertEqual(frames_sent, frames_expected) + + def assertFrameReceived(self, connection, frame): + """ + Incoming data for ``connection`` contains the given frame. + + ``frame`` may be ``None`` if no frame is expected. + + """ + frames_received = connection.events_received() + frames_expected = [] if frame is None else [frame] + self.assertEqual(frames_received, frames_expected) + + def assertConnectionClosing(self, connection, code=None, reason=""): + """ + Incoming data caused the "Start the WebSocket Closing Handshake" process. + + """ + close_frame = Frame( + OP_CLOSE, + b"" if code is None else Close(code, reason).serialize(), + ) + # A close frame was received. + self.assertFrameReceived(connection, close_frame) + # A close frame and possibly the end of stream were sent. + self.assertFrameSent(connection, close_frame, eof=connection.side is SERVER) + + def assertConnectionFailing(self, connection, code=None, reason=""): + """ + Incoming data caused the "Fail the WebSocket Connection" process. + + """ + close_frame = Frame( + OP_CLOSE, + b"" if code is None else Close(code, reason).serialize(), + ) + # No frame was received. + self.assertFrameReceived(connection, None) + # A close frame and possibly the end of stream were sent. + self.assertFrameSent(connection, close_frame, eof=connection.side is SERVER) + + +class MaskingTests(ProtocolTestCase): + """ + Test frame masking. + + 5.1. Overview + + """ + + unmasked_text_frame_date = b"\x81\x04Spam" + masked_text_frame_data = b"\x81\x84\x00\xff\x00\xff\x53\x8f\x61\x92" + + def test_client_sends_masked_frame(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\xff\x00\xff"): + client.send_text(b"Spam", True) + self.assertEqual(client.data_to_send(), [self.masked_text_frame_data]) + + def test_server_sends_unmasked_frame(self): + server = Protocol(SERVER) + server.send_text(b"Spam", True) + self.assertEqual(server.data_to_send(), [self.unmasked_text_frame_date]) + + def test_client_receives_unmasked_frame(self): + client = Protocol(CLIENT) + client.receive_data(self.unmasked_text_frame_date) + self.assertFrameReceived( + client, + Frame(OP_TEXT, b"Spam"), + ) + + def test_server_receives_masked_frame(self): + server = Protocol(SERVER) + server.receive_data(self.masked_text_frame_data) + self.assertFrameReceived( + server, + Frame(OP_TEXT, b"Spam"), + ) + + def test_client_receives_masked_frame(self): + client = Protocol(CLIENT) + client.receive_data(self.masked_text_frame_data) + self.assertIsInstance(client.parser_exc, ProtocolError) + self.assertEqual(str(client.parser_exc), "incorrect masking") + self.assertConnectionFailing( + client, CloseCode.PROTOCOL_ERROR, "incorrect masking" + ) + + def test_server_receives_unmasked_frame(self): + server = Protocol(SERVER) + server.receive_data(self.unmasked_text_frame_date) + self.assertIsInstance(server.parser_exc, ProtocolError) + self.assertEqual(str(server.parser_exc), "incorrect masking") + self.assertConnectionFailing( + server, CloseCode.PROTOCOL_ERROR, "incorrect masking" + ) + + +class ContinuationTests(ProtocolTestCase): + """ + Test continuation frames without text or binary frames. + + """ + + def test_client_sends_unexpected_continuation(self): + client = Protocol(CLIENT) + with self.assertRaises(ProtocolError) as raised: + client.send_continuation(b"", fin=False) + self.assertEqual(str(raised.exception), "unexpected continuation frame") + + def test_server_sends_unexpected_continuation(self): + server = Protocol(SERVER) + with self.assertRaises(ProtocolError) as raised: + server.send_continuation(b"", fin=False) + self.assertEqual(str(raised.exception), "unexpected continuation frame") + + def test_client_receives_unexpected_continuation(self): + client = Protocol(CLIENT) + client.receive_data(b"\x00\x00") + self.assertIsInstance(client.parser_exc, ProtocolError) + self.assertEqual(str(client.parser_exc), "unexpected continuation frame") + self.assertConnectionFailing( + client, CloseCode.PROTOCOL_ERROR, "unexpected continuation frame" + ) + + def test_server_receives_unexpected_continuation(self): + server = Protocol(SERVER) + server.receive_data(b"\x00\x80\x00\x00\x00\x00") + self.assertIsInstance(server.parser_exc, ProtocolError) + self.assertEqual(str(server.parser_exc), "unexpected continuation frame") + self.assertConnectionFailing( + server, CloseCode.PROTOCOL_ERROR, "unexpected continuation frame" + ) + + def test_client_sends_continuation_after_sending_close(self): + client = Protocol(CLIENT) + # Since it isn't possible to send a close frame in a fragmented + # message (see test_client_send_close_in_fragmented_message), in fact, + # this is the same test as test_client_sends_unexpected_continuation. + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_close(CloseCode.GOING_AWAY) + self.assertEqual(client.data_to_send(), [b"\x88\x82\x00\x00\x00\x00\x03\xe9"]) + with self.assertRaises(ProtocolError) as raised: + client.send_continuation(b"", fin=False) + self.assertEqual(str(raised.exception), "unexpected continuation frame") + + def test_server_sends_continuation_after_sending_close(self): + # Since it isn't possible to send a close frame in a fragmented + # message (see test_server_send_close_in_fragmented_message), in fact, + # this is the same test as test_server_sends_unexpected_continuation. + server = Protocol(SERVER) + server.send_close(CloseCode.NORMAL_CLOSURE) + self.assertEqual(server.data_to_send(), [b"\x88\x02\x03\xe8"]) + with self.assertRaises(ProtocolError) as raised: + server.send_continuation(b"", fin=False) + self.assertEqual(str(raised.exception), "unexpected continuation frame") + + def test_client_receives_continuation_after_receiving_close(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x02\x03\xe8") + self.assertConnectionClosing(client, CloseCode.NORMAL_CLOSURE) + client.receive_data(b"\x00\x00") + self.assertFrameReceived(client, None) + self.assertFrameSent(client, None) + + def test_server_receives_continuation_after_receiving_close(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe9") + self.assertConnectionClosing(server, CloseCode.GOING_AWAY) + server.receive_data(b"\x00\x80\x00\xff\x00\xff") + self.assertFrameReceived(server, None) + self.assertFrameSent(server, None) + + +class TextTests(ProtocolTestCase): + """ + Test text frames and continuation frames. + + """ + + def test_client_sends_text(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_text("😀".encode()) + self.assertEqual( + client.data_to_send(), [b"\x81\x84\x00\x00\x00\x00\xf0\x9f\x98\x80"] + ) + + def test_server_sends_text(self): + server = Protocol(SERVER) + server.send_text("😀".encode()) + self.assertEqual(server.data_to_send(), [b"\x81\x04\xf0\x9f\x98\x80"]) + + def test_client_receives_text(self): + client = Protocol(CLIENT) + client.receive_data(b"\x81\x04\xf0\x9f\x98\x80") + self.assertFrameReceived( + client, + Frame(OP_TEXT, "😀".encode()), + ) + + def test_server_receives_text(self): + server = Protocol(SERVER) + server.receive_data(b"\x81\x84\x00\x00\x00\x00\xf0\x9f\x98\x80") + self.assertFrameReceived( + server, + Frame(OP_TEXT, "😀".encode()), + ) + + def test_client_receives_text_over_size_limit(self): + client = Protocol(CLIENT, max_size=3) + client.receive_data(b"\x81\x04\xf0\x9f\x98\x80") + self.assertIsInstance(client.parser_exc, PayloadTooBig) + self.assertEqual(str(client.parser_exc), "over size limit (4 > 3 bytes)") + self.assertConnectionFailing( + client, CloseCode.MESSAGE_TOO_BIG, "over size limit (4 > 3 bytes)" + ) + + def test_server_receives_text_over_size_limit(self): + server = Protocol(SERVER, max_size=3) + server.receive_data(b"\x81\x84\x00\x00\x00\x00\xf0\x9f\x98\x80") + self.assertIsInstance(server.parser_exc, PayloadTooBig) + self.assertEqual(str(server.parser_exc), "over size limit (4 > 3 bytes)") + self.assertConnectionFailing( + server, CloseCode.MESSAGE_TOO_BIG, "over size limit (4 > 3 bytes)" + ) + + def test_client_receives_text_without_size_limit(self): + client = Protocol(CLIENT, max_size=None) + client.receive_data(b"\x81\x04\xf0\x9f\x98\x80") + self.assertFrameReceived( + client, + Frame(OP_TEXT, "😀".encode()), + ) + + def test_server_receives_text_without_size_limit(self): + server = Protocol(SERVER, max_size=None) + server.receive_data(b"\x81\x84\x00\x00\x00\x00\xf0\x9f\x98\x80") + self.assertFrameReceived( + server, + Frame(OP_TEXT, "😀".encode()), + ) + + def test_client_sends_fragmented_text(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_text("😀".encode()[:2], fin=False) + self.assertEqual(client.data_to_send(), [b"\x01\x82\x00\x00\x00\x00\xf0\x9f"]) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_continuation("😀😀".encode()[2:6], fin=False) + self.assertEqual( + client.data_to_send(), [b"\x00\x84\x00\x00\x00\x00\x98\x80\xf0\x9f"] + ) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_continuation("😀".encode()[2:], fin=True) + self.assertEqual(client.data_to_send(), [b"\x80\x82\x00\x00\x00\x00\x98\x80"]) + + def test_server_sends_fragmented_text(self): + server = Protocol(SERVER) + server.send_text("😀".encode()[:2], fin=False) + self.assertEqual(server.data_to_send(), [b"\x01\x02\xf0\x9f"]) + server.send_continuation("😀😀".encode()[2:6], fin=False) + self.assertEqual(server.data_to_send(), [b"\x00\x04\x98\x80\xf0\x9f"]) + server.send_continuation("😀".encode()[2:], fin=True) + self.assertEqual(server.data_to_send(), [b"\x80\x02\x98\x80"]) + + def test_client_receives_fragmented_text(self): + client = Protocol(CLIENT) + client.receive_data(b"\x01\x02\xf0\x9f") + self.assertFrameReceived( + client, + Frame(OP_TEXT, "😀".encode()[:2], fin=False), + ) + client.receive_data(b"\x00\x04\x98\x80\xf0\x9f") + self.assertFrameReceived( + client, + Frame(OP_CONT, "😀😀".encode()[2:6], fin=False), + ) + client.receive_data(b"\x80\x02\x98\x80") + self.assertFrameReceived( + client, + Frame(OP_CONT, "😀".encode()[2:]), + ) + + def test_server_receives_fragmented_text(self): + server = Protocol(SERVER) + server.receive_data(b"\x01\x82\x00\x00\x00\x00\xf0\x9f") + self.assertFrameReceived( + server, + Frame(OP_TEXT, "😀".encode()[:2], fin=False), + ) + server.receive_data(b"\x00\x84\x00\x00\x00\x00\x98\x80\xf0\x9f") + self.assertFrameReceived( + server, + Frame(OP_CONT, "😀😀".encode()[2:6], fin=False), + ) + server.receive_data(b"\x80\x82\x00\x00\x00\x00\x98\x80") + self.assertFrameReceived( + server, + Frame(OP_CONT, "😀".encode()[2:]), + ) + + def test_client_receives_fragmented_text_over_size_limit(self): + client = Protocol(CLIENT, max_size=3) + client.receive_data(b"\x01\x02\xf0\x9f") + self.assertFrameReceived( + client, + Frame(OP_TEXT, "😀".encode()[:2], fin=False), + ) + client.receive_data(b"\x80\x02\x98\x80") + self.assertIsInstance(client.parser_exc, PayloadTooBig) + self.assertEqual(str(client.parser_exc), "over size limit (2 > 1 bytes)") + self.assertConnectionFailing( + client, CloseCode.MESSAGE_TOO_BIG, "over size limit (2 > 1 bytes)" + ) + + def test_server_receives_fragmented_text_over_size_limit(self): + server = Protocol(SERVER, max_size=3) + server.receive_data(b"\x01\x82\x00\x00\x00\x00\xf0\x9f") + self.assertFrameReceived( + server, + Frame(OP_TEXT, "😀".encode()[:2], fin=False), + ) + server.receive_data(b"\x80\x82\x00\x00\x00\x00\x98\x80") + self.assertIsInstance(server.parser_exc, PayloadTooBig) + self.assertEqual(str(server.parser_exc), "over size limit (2 > 1 bytes)") + self.assertConnectionFailing( + server, CloseCode.MESSAGE_TOO_BIG, "over size limit (2 > 1 bytes)" + ) + + def test_client_receives_fragmented_text_without_size_limit(self): + client = Protocol(CLIENT, max_size=None) + client.receive_data(b"\x01\x02\xf0\x9f") + self.assertFrameReceived( + client, + Frame(OP_TEXT, "😀".encode()[:2], fin=False), + ) + client.receive_data(b"\x00\x04\x98\x80\xf0\x9f") + self.assertFrameReceived( + client, + Frame(OP_CONT, "😀😀".encode()[2:6], fin=False), + ) + client.receive_data(b"\x80\x02\x98\x80") + self.assertFrameReceived( + client, + Frame(OP_CONT, "😀".encode()[2:]), + ) + + def test_server_receives_fragmented_text_without_size_limit(self): + server = Protocol(SERVER, max_size=None) + server.receive_data(b"\x01\x82\x00\x00\x00\x00\xf0\x9f") + self.assertFrameReceived( + server, + Frame(OP_TEXT, "😀".encode()[:2], fin=False), + ) + server.receive_data(b"\x00\x84\x00\x00\x00\x00\x98\x80\xf0\x9f") + self.assertFrameReceived( + server, + Frame(OP_CONT, "😀😀".encode()[2:6], fin=False), + ) + server.receive_data(b"\x80\x82\x00\x00\x00\x00\x98\x80") + self.assertFrameReceived( + server, + Frame(OP_CONT, "😀".encode()[2:]), + ) + + def test_client_sends_unexpected_text(self): + client = Protocol(CLIENT) + client.send_text(b"", fin=False) + with self.assertRaises(ProtocolError) as raised: + client.send_text(b"", fin=False) + self.assertEqual(str(raised.exception), "expected a continuation frame") + + def test_server_sends_unexpected_text(self): + server = Protocol(SERVER) + server.send_text(b"", fin=False) + with self.assertRaises(ProtocolError) as raised: + server.send_text(b"", fin=False) + self.assertEqual(str(raised.exception), "expected a continuation frame") + + def test_client_receives_unexpected_text(self): + client = Protocol(CLIENT) + client.receive_data(b"\x01\x00") + self.assertFrameReceived( + client, + Frame(OP_TEXT, b"", fin=False), + ) + client.receive_data(b"\x01\x00") + self.assertIsInstance(client.parser_exc, ProtocolError) + self.assertEqual(str(client.parser_exc), "expected a continuation frame") + self.assertConnectionFailing( + client, CloseCode.PROTOCOL_ERROR, "expected a continuation frame" + ) + + def test_server_receives_unexpected_text(self): + server = Protocol(SERVER) + server.receive_data(b"\x01\x80\x00\x00\x00\x00") + self.assertFrameReceived( + server, + Frame(OP_TEXT, b"", fin=False), + ) + server.receive_data(b"\x01\x80\x00\x00\x00\x00") + self.assertIsInstance(server.parser_exc, ProtocolError) + self.assertEqual(str(server.parser_exc), "expected a continuation frame") + self.assertConnectionFailing( + server, CloseCode.PROTOCOL_ERROR, "expected a continuation frame" + ) + + def test_client_sends_text_after_sending_close(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_close(CloseCode.GOING_AWAY) + self.assertEqual(client.data_to_send(), [b"\x88\x82\x00\x00\x00\x00\x03\xe9"]) + with self.assertRaises(InvalidState): + client.send_text(b"") + + def test_server_sends_text_after_sending_close(self): + server = Protocol(SERVER) + server.send_close(CloseCode.NORMAL_CLOSURE) + self.assertEqual(server.data_to_send(), [b"\x88\x02\x03\xe8"]) + with self.assertRaises(InvalidState): + server.send_text(b"") + + def test_client_receives_text_after_receiving_close(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x02\x03\xe8") + self.assertConnectionClosing(client, CloseCode.NORMAL_CLOSURE) + client.receive_data(b"\x81\x00") + self.assertFrameReceived(client, None) + self.assertFrameSent(client, None) + + def test_server_receives_text_after_receiving_close(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe9") + self.assertConnectionClosing(server, CloseCode.GOING_AWAY) + server.receive_data(b"\x81\x80\x00\xff\x00\xff") + self.assertFrameReceived(server, None) + self.assertFrameSent(server, None) + + +class BinaryTests(ProtocolTestCase): + """ + Test binary frames and continuation frames. + + """ + + def test_client_sends_binary(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_binary(b"\x01\x02\xfe\xff") + self.assertEqual( + client.data_to_send(), [b"\x82\x84\x00\x00\x00\x00\x01\x02\xfe\xff"] + ) + + def test_server_sends_binary(self): + server = Protocol(SERVER) + server.send_binary(b"\x01\x02\xfe\xff") + self.assertEqual(server.data_to_send(), [b"\x82\x04\x01\x02\xfe\xff"]) + + def test_client_receives_binary(self): + client = Protocol(CLIENT) + client.receive_data(b"\x82\x04\x01\x02\xfe\xff") + self.assertFrameReceived( + client, + Frame(OP_BINARY, b"\x01\x02\xfe\xff"), + ) + + def test_server_receives_binary(self): + server = Protocol(SERVER) + server.receive_data(b"\x82\x84\x00\x00\x00\x00\x01\x02\xfe\xff") + self.assertFrameReceived( + server, + Frame(OP_BINARY, b"\x01\x02\xfe\xff"), + ) + + def test_client_receives_binary_over_size_limit(self): + client = Protocol(CLIENT, max_size=3) + client.receive_data(b"\x82\x04\x01\x02\xfe\xff") + self.assertIsInstance(client.parser_exc, PayloadTooBig) + self.assertEqual(str(client.parser_exc), "over size limit (4 > 3 bytes)") + self.assertConnectionFailing( + client, CloseCode.MESSAGE_TOO_BIG, "over size limit (4 > 3 bytes)" + ) + + def test_server_receives_binary_over_size_limit(self): + server = Protocol(SERVER, max_size=3) + server.receive_data(b"\x82\x84\x00\x00\x00\x00\x01\x02\xfe\xff") + self.assertIsInstance(server.parser_exc, PayloadTooBig) + self.assertEqual(str(server.parser_exc), "over size limit (4 > 3 bytes)") + self.assertConnectionFailing( + server, CloseCode.MESSAGE_TOO_BIG, "over size limit (4 > 3 bytes)" + ) + + def test_client_sends_fragmented_binary(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_binary(b"\x01\x02", fin=False) + self.assertEqual(client.data_to_send(), [b"\x02\x82\x00\x00\x00\x00\x01\x02"]) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_continuation(b"\xee\xff\x01\x02", fin=False) + self.assertEqual( + client.data_to_send(), [b"\x00\x84\x00\x00\x00\x00\xee\xff\x01\x02"] + ) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_continuation(b"\xee\xff", fin=True) + self.assertEqual(client.data_to_send(), [b"\x80\x82\x00\x00\x00\x00\xee\xff"]) + + def test_server_sends_fragmented_binary(self): + server = Protocol(SERVER) + server.send_binary(b"\x01\x02", fin=False) + self.assertEqual(server.data_to_send(), [b"\x02\x02\x01\x02"]) + server.send_continuation(b"\xee\xff\x01\x02", fin=False) + self.assertEqual(server.data_to_send(), [b"\x00\x04\xee\xff\x01\x02"]) + server.send_continuation(b"\xee\xff", fin=True) + self.assertEqual(server.data_to_send(), [b"\x80\x02\xee\xff"]) + + def test_client_receives_fragmented_binary(self): + client = Protocol(CLIENT) + client.receive_data(b"\x02\x02\x01\x02") + self.assertFrameReceived( + client, + Frame(OP_BINARY, b"\x01\x02", fin=False), + ) + client.receive_data(b"\x00\x04\xfe\xff\x01\x02") + self.assertFrameReceived( + client, + Frame(OP_CONT, b"\xfe\xff\x01\x02", fin=False), + ) + client.receive_data(b"\x80\x02\xfe\xff") + self.assertFrameReceived( + client, + Frame(OP_CONT, b"\xfe\xff"), + ) + + def test_server_receives_fragmented_binary(self): + server = Protocol(SERVER) + server.receive_data(b"\x02\x82\x00\x00\x00\x00\x01\x02") + self.assertFrameReceived( + server, + Frame(OP_BINARY, b"\x01\x02", fin=False), + ) + server.receive_data(b"\x00\x84\x00\x00\x00\x00\xee\xff\x01\x02") + self.assertFrameReceived( + server, + Frame(OP_CONT, b"\xee\xff\x01\x02", fin=False), + ) + server.receive_data(b"\x80\x82\x00\x00\x00\x00\xfe\xff") + self.assertFrameReceived( + server, + Frame(OP_CONT, b"\xfe\xff"), + ) + + def test_client_receives_fragmented_binary_over_size_limit(self): + client = Protocol(CLIENT, max_size=3) + client.receive_data(b"\x02\x02\x01\x02") + self.assertFrameReceived( + client, + Frame(OP_BINARY, b"\x01\x02", fin=False), + ) + client.receive_data(b"\x80\x02\xfe\xff") + self.assertIsInstance(client.parser_exc, PayloadTooBig) + self.assertEqual(str(client.parser_exc), "over size limit (2 > 1 bytes)") + self.assertConnectionFailing( + client, CloseCode.MESSAGE_TOO_BIG, "over size limit (2 > 1 bytes)" + ) + + def test_server_receives_fragmented_binary_over_size_limit(self): + server = Protocol(SERVER, max_size=3) + server.receive_data(b"\x02\x82\x00\x00\x00\x00\x01\x02") + self.assertFrameReceived( + server, + Frame(OP_BINARY, b"\x01\x02", fin=False), + ) + server.receive_data(b"\x80\x82\x00\x00\x00\x00\xfe\xff") + self.assertIsInstance(server.parser_exc, PayloadTooBig) + self.assertEqual(str(server.parser_exc), "over size limit (2 > 1 bytes)") + self.assertConnectionFailing( + server, CloseCode.MESSAGE_TOO_BIG, "over size limit (2 > 1 bytes)" + ) + + def test_client_sends_unexpected_binary(self): + client = Protocol(CLIENT) + client.send_binary(b"", fin=False) + with self.assertRaises(ProtocolError) as raised: + client.send_binary(b"", fin=False) + self.assertEqual(str(raised.exception), "expected a continuation frame") + + def test_server_sends_unexpected_binary(self): + server = Protocol(SERVER) + server.send_binary(b"", fin=False) + with self.assertRaises(ProtocolError) as raised: + server.send_binary(b"", fin=False) + self.assertEqual(str(raised.exception), "expected a continuation frame") + + def test_client_receives_unexpected_binary(self): + client = Protocol(CLIENT) + client.receive_data(b"\x02\x00") + self.assertFrameReceived( + client, + Frame(OP_BINARY, b"", fin=False), + ) + client.receive_data(b"\x02\x00") + self.assertIsInstance(client.parser_exc, ProtocolError) + self.assertEqual(str(client.parser_exc), "expected a continuation frame") + self.assertConnectionFailing( + client, CloseCode.PROTOCOL_ERROR, "expected a continuation frame" + ) + + def test_server_receives_unexpected_binary(self): + server = Protocol(SERVER) + server.receive_data(b"\x02\x80\x00\x00\x00\x00") + self.assertFrameReceived( + server, + Frame(OP_BINARY, b"", fin=False), + ) + server.receive_data(b"\x02\x80\x00\x00\x00\x00") + self.assertIsInstance(server.parser_exc, ProtocolError) + self.assertEqual(str(server.parser_exc), "expected a continuation frame") + self.assertConnectionFailing( + server, CloseCode.PROTOCOL_ERROR, "expected a continuation frame" + ) + + def test_client_sends_binary_after_sending_close(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_close(CloseCode.GOING_AWAY) + self.assertEqual(client.data_to_send(), [b"\x88\x82\x00\x00\x00\x00\x03\xe9"]) + with self.assertRaises(InvalidState): + client.send_binary(b"") + + def test_server_sends_binary_after_sending_close(self): + server = Protocol(SERVER) + server.send_close(CloseCode.NORMAL_CLOSURE) + self.assertEqual(server.data_to_send(), [b"\x88\x02\x03\xe8"]) + with self.assertRaises(InvalidState): + server.send_binary(b"") + + def test_client_receives_binary_after_receiving_close(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x02\x03\xe8") + self.assertConnectionClosing(client, CloseCode.NORMAL_CLOSURE) + client.receive_data(b"\x82\x00") + self.assertFrameReceived(client, None) + self.assertFrameSent(client, None) + + def test_server_receives_binary_after_receiving_close(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe9") + self.assertConnectionClosing(server, CloseCode.GOING_AWAY) + server.receive_data(b"\x82\x80\x00\xff\x00\xff") + self.assertFrameReceived(server, None) + self.assertFrameSent(server, None) + + +class CloseTests(ProtocolTestCase): + """ + Test close frames. + + See RFC 6544: + + 5.5.1. Close + 7.1.6. The WebSocket Connection Close Reason + 7.1.7. Fail the WebSocket Connection + + """ + + def test_close_code(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x04\x03\xe8OK") + client.receive_eof() + self.assertEqual(client.close_code, CloseCode.NORMAL_CLOSURE) + + def test_close_reason(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x84\x00\x00\x00\x00\x03\xe8OK") + server.receive_eof() + self.assertEqual(server.close_reason, "OK") + + def test_close_code_not_provided(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x80\x00\x00\x00\x00") + server.receive_eof() + self.assertEqual(server.close_code, CloseCode.NO_STATUS_RCVD) + + def test_close_reason_not_provided(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x00") + client.receive_eof() + self.assertEqual(client.close_reason, "") + + def test_close_code_not_available(self): + client = Protocol(CLIENT) + client.receive_eof() + self.assertEqual(client.close_code, CloseCode.ABNORMAL_CLOSURE) + + def test_close_reason_not_available(self): + server = Protocol(SERVER) + server.receive_eof() + self.assertEqual(server.close_reason, "") + + def test_close_code_not_available_yet(self): + server = Protocol(SERVER) + self.assertIsNone(server.close_code) + + def test_close_reason_not_available_yet(self): + client = Protocol(CLIENT) + self.assertIsNone(client.close_reason) + + def test_client_sends_close(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x3c\x3c\x3c\x3c"): + client.send_close() + self.assertEqual(client.data_to_send(), [b"\x88\x80\x3c\x3c\x3c\x3c"]) + self.assertIs(client.state, CLOSING) + + def test_server_sends_close(self): + server = Protocol(SERVER) + server.send_close() + self.assertEqual(server.data_to_send(), [b"\x88\x00"]) + self.assertIs(server.state, CLOSING) + + def test_client_receives_close(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x3c\x3c\x3c\x3c"): + client.receive_data(b"\x88\x00") + self.assertEqual(client.events_received(), [Frame(OP_CLOSE, b"")]) + self.assertEqual(client.data_to_send(), [b"\x88\x80\x3c\x3c\x3c\x3c"]) + self.assertIs(client.state, CLOSING) + + def test_server_receives_close(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c") + self.assertEqual(server.events_received(), [Frame(OP_CLOSE, b"")]) + self.assertEqual(server.data_to_send(), [b"\x88\x00", b""]) + self.assertIs(server.state, CLOSING) + + def test_client_sends_close_then_receives_close(self): + # Client-initiated close handshake on the client side. + client = Protocol(CLIENT) + + client.send_close() + self.assertFrameReceived(client, None) + self.assertFrameSent(client, Frame(OP_CLOSE, b"")) + + client.receive_data(b"\x88\x00") + self.assertFrameReceived(client, Frame(OP_CLOSE, b"")) + self.assertFrameSent(client, None) + + client.receive_eof() + self.assertFrameReceived(client, None) + self.assertFrameSent(client, None, eof=True) + + def test_server_sends_close_then_receives_close(self): + # Server-initiated close handshake on the server side. + server = Protocol(SERVER) + + server.send_close() + self.assertFrameReceived(server, None) + self.assertFrameSent(server, Frame(OP_CLOSE, b"")) + + server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c") + self.assertFrameReceived(server, Frame(OP_CLOSE, b"")) + self.assertFrameSent(server, None, eof=True) + + server.receive_eof() + self.assertFrameReceived(server, None) + self.assertFrameSent(server, None) + + def test_client_receives_close_then_sends_close(self): + # Server-initiated close handshake on the client side. + client = Protocol(CLIENT) + + client.receive_data(b"\x88\x00") + self.assertFrameReceived(client, Frame(OP_CLOSE, b"")) + self.assertFrameSent(client, Frame(OP_CLOSE, b"")) + + client.receive_eof() + self.assertFrameReceived(client, None) + self.assertFrameSent(client, None, eof=True) + + def test_server_receives_close_then_sends_close(self): + # Client-initiated close handshake on the server side. + server = Protocol(SERVER) + + server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c") + self.assertFrameReceived(server, Frame(OP_CLOSE, b"")) + self.assertFrameSent(server, Frame(OP_CLOSE, b""), eof=True) + + server.receive_eof() + self.assertFrameReceived(server, None) + self.assertFrameSent(server, None) + + def test_client_sends_close_with_code(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_close(CloseCode.GOING_AWAY) + self.assertEqual(client.data_to_send(), [b"\x88\x82\x00\x00\x00\x00\x03\xe9"]) + self.assertIs(client.state, CLOSING) + + def test_server_sends_close_with_code(self): + server = Protocol(SERVER) + server.send_close(CloseCode.NORMAL_CLOSURE) + self.assertEqual(server.data_to_send(), [b"\x88\x02\x03\xe8"]) + self.assertIs(server.state, CLOSING) + + def test_client_receives_close_with_code(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x02\x03\xe8") + self.assertConnectionClosing(client, CloseCode.NORMAL_CLOSURE, "") + self.assertIs(client.state, CLOSING) + + def test_server_receives_close_with_code(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe9") + self.assertConnectionClosing(server, CloseCode.GOING_AWAY, "") + self.assertIs(server.state, CLOSING) + + def test_client_sends_close_with_code_and_reason(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_close(CloseCode.GOING_AWAY, "going away") + self.assertEqual( + client.data_to_send(), [b"\x88\x8c\x00\x00\x00\x00\x03\xe9going away"] + ) + self.assertIs(client.state, CLOSING) + + def test_server_sends_close_with_code_and_reason(self): + server = Protocol(SERVER) + server.send_close(CloseCode.NORMAL_CLOSURE, "OK") + self.assertEqual(server.data_to_send(), [b"\x88\x04\x03\xe8OK"]) + self.assertIs(server.state, CLOSING) + + def test_client_receives_close_with_code_and_reason(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x04\x03\xe8OK") + self.assertConnectionClosing(client, CloseCode.NORMAL_CLOSURE, "OK") + self.assertIs(client.state, CLOSING) + + def test_server_receives_close_with_code_and_reason(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x8c\x00\x00\x00\x00\x03\xe9going away") + self.assertConnectionClosing(server, CloseCode.GOING_AWAY, "going away") + self.assertIs(server.state, CLOSING) + + def test_client_sends_close_with_reason_only(self): + client = Protocol(CLIENT) + with self.assertRaises(ProtocolError) as raised: + client.send_close(reason="going away") + self.assertEqual(str(raised.exception), "cannot send a reason without a code") + + def test_server_sends_close_with_reason_only(self): + server = Protocol(SERVER) + with self.assertRaises(ProtocolError) as raised: + server.send_close(reason="OK") + self.assertEqual(str(raised.exception), "cannot send a reason without a code") + + def test_client_receives_close_with_truncated_code(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x01\x03") + self.assertIsInstance(client.parser_exc, ProtocolError) + self.assertEqual(str(client.parser_exc), "close frame too short") + self.assertConnectionFailing( + client, CloseCode.PROTOCOL_ERROR, "close frame too short" + ) + self.assertIs(client.state, CLOSING) + + def test_server_receives_close_with_truncated_code(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x81\x00\x00\x00\x00\x03") + self.assertIsInstance(server.parser_exc, ProtocolError) + self.assertEqual(str(server.parser_exc), "close frame too short") + self.assertConnectionFailing( + server, CloseCode.PROTOCOL_ERROR, "close frame too short" + ) + self.assertIs(server.state, CLOSING) + + def test_client_receives_close_with_non_utf8_reason(self): + client = Protocol(CLIENT) + + client.receive_data(b"\x88\x04\x03\xe8\xff\xff") + self.assertIsInstance(client.parser_exc, UnicodeDecodeError) + self.assertEqual( + str(client.parser_exc), + "'utf-8' codec can't decode byte 0xff in position 0: invalid start byte", + ) + self.assertConnectionFailing( + client, CloseCode.INVALID_DATA, "invalid start byte at position 0" + ) + self.assertIs(client.state, CLOSING) + + def test_server_receives_close_with_non_utf8_reason(self): + server = Protocol(SERVER) + + server.receive_data(b"\x88\x84\x00\x00\x00\x00\x03\xe9\xff\xff") + self.assertIsInstance(server.parser_exc, UnicodeDecodeError) + self.assertEqual( + str(server.parser_exc), + "'utf-8' codec can't decode byte 0xff in position 0: invalid start byte", + ) + self.assertConnectionFailing( + server, CloseCode.INVALID_DATA, "invalid start byte at position 0" + ) + self.assertIs(server.state, CLOSING) + + +class PingTests(ProtocolTestCase): + """ + Test ping. See 5.5.2. Ping in RFC 6544. + + """ + + def test_client_sends_ping(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x44\x88\xcc"): + client.send_ping(b"") + self.assertEqual(client.data_to_send(), [b"\x89\x80\x00\x44\x88\xcc"]) + + def test_server_sends_ping(self): + server = Protocol(SERVER) + server.send_ping(b"") + self.assertEqual(server.data_to_send(), [b"\x89\x00"]) + + def test_client_receives_ping(self): + client = Protocol(CLIENT) + client.receive_data(b"\x89\x00") + self.assertFrameReceived( + client, + Frame(OP_PING, b""), + ) + self.assertFrameSent( + client, + Frame(OP_PONG, b""), + ) + + def test_server_receives_ping(self): + server = Protocol(SERVER) + server.receive_data(b"\x89\x80\x00\x44\x88\xcc") + self.assertFrameReceived( + server, + Frame(OP_PING, b""), + ) + self.assertFrameSent( + server, + Frame(OP_PONG, b""), + ) + + def test_client_sends_ping_with_data(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x44\x88\xcc"): + client.send_ping(b"\x22\x66\xaa\xee") + self.assertEqual( + client.data_to_send(), [b"\x89\x84\x00\x44\x88\xcc\x22\x22\x22\x22"] + ) + + def test_server_sends_ping_with_data(self): + server = Protocol(SERVER) + server.send_ping(b"\x22\x66\xaa\xee") + self.assertEqual(server.data_to_send(), [b"\x89\x04\x22\x66\xaa\xee"]) + + def test_client_receives_ping_with_data(self): + client = Protocol(CLIENT) + client.receive_data(b"\x89\x04\x22\x66\xaa\xee") + self.assertFrameReceived( + client, + Frame(OP_PING, b"\x22\x66\xaa\xee"), + ) + self.assertFrameSent( + client, + Frame(OP_PONG, b"\x22\x66\xaa\xee"), + ) + + def test_server_receives_ping_with_data(self): + server = Protocol(SERVER) + server.receive_data(b"\x89\x84\x00\x44\x88\xcc\x22\x22\x22\x22") + self.assertFrameReceived( + server, + Frame(OP_PING, b"\x22\x66\xaa\xee"), + ) + self.assertFrameSent( + server, + Frame(OP_PONG, b"\x22\x66\xaa\xee"), + ) + + def test_client_sends_fragmented_ping_frame(self): + client = Protocol(CLIENT) + # This is only possible through a private API. + with self.assertRaises(ProtocolError) as raised: + client.send_frame(Frame(OP_PING, b"", fin=False)) + self.assertEqual(str(raised.exception), "fragmented control frame") + + def test_server_sends_fragmented_ping_frame(self): + server = Protocol(SERVER) + # This is only possible through a private API. + with self.assertRaises(ProtocolError) as raised: + server.send_frame(Frame(OP_PING, b"", fin=False)) + self.assertEqual(str(raised.exception), "fragmented control frame") + + def test_client_receives_fragmented_ping_frame(self): + client = Protocol(CLIENT) + client.receive_data(b"\x09\x00") + self.assertIsInstance(client.parser_exc, ProtocolError) + self.assertEqual(str(client.parser_exc), "fragmented control frame") + self.assertConnectionFailing( + client, CloseCode.PROTOCOL_ERROR, "fragmented control frame" + ) + + def test_server_receives_fragmented_ping_frame(self): + server = Protocol(SERVER) + server.receive_data(b"\x09\x80\x3c\x3c\x3c\x3c") + self.assertIsInstance(server.parser_exc, ProtocolError) + self.assertEqual(str(server.parser_exc), "fragmented control frame") + self.assertConnectionFailing( + server, CloseCode.PROTOCOL_ERROR, "fragmented control frame" + ) + + def test_client_sends_ping_after_sending_close(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_close(CloseCode.GOING_AWAY) + self.assertEqual(client.data_to_send(), [b"\x88\x82\x00\x00\x00\x00\x03\xe9"]) + # The spec says: "An endpoint MAY send a Ping frame any time (...) + # before the connection is closed" but websockets doesn't support + # sending a Ping frame after a Close frame. + with self.assertRaises(InvalidState) as raised: + client.send_ping(b"") + self.assertEqual( + str(raised.exception), + "cannot write to a WebSocket in the CLOSING state", + ) + + def test_server_sends_ping_after_sending_close(self): + server = Protocol(SERVER) + server.send_close(CloseCode.NORMAL_CLOSURE) + self.assertEqual(server.data_to_send(), [b"\x88\x02\x03\xe8"]) + # The spec says: "An endpoint MAY send a Ping frame any time (...) + # before the connection is closed" but websockets doesn't support + # sending a Ping frame after a Close frame. + with self.assertRaises(InvalidState) as raised: + server.send_ping(b"") + self.assertEqual( + str(raised.exception), + "cannot write to a WebSocket in the CLOSING state", + ) + + def test_client_receives_ping_after_receiving_close(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x02\x03\xe8") + self.assertConnectionClosing(client, CloseCode.NORMAL_CLOSURE) + client.receive_data(b"\x89\x04\x22\x66\xaa\xee") + self.assertFrameReceived(client, None) + self.assertFrameSent(client, None) + + def test_server_receives_ping_after_receiving_close(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe9") + self.assertConnectionClosing(server, CloseCode.GOING_AWAY) + server.receive_data(b"\x89\x84\x00\x44\x88\xcc\x22\x22\x22\x22") + self.assertFrameReceived(server, None) + self.assertFrameSent(server, None) + + +class PongTests(ProtocolTestCase): + """ + Test pong frames. See 5.5.3. Pong in RFC 6544. + + """ + + def test_client_sends_pong(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x44\x88\xcc"): + client.send_pong(b"") + self.assertEqual(client.data_to_send(), [b"\x8a\x80\x00\x44\x88\xcc"]) + + def test_server_sends_pong(self): + server = Protocol(SERVER) + server.send_pong(b"") + self.assertEqual(server.data_to_send(), [b"\x8a\x00"]) + + def test_client_receives_pong(self): + client = Protocol(CLIENT) + client.receive_data(b"\x8a\x00") + self.assertFrameReceived( + client, + Frame(OP_PONG, b""), + ) + + def test_server_receives_pong(self): + server = Protocol(SERVER) + server.receive_data(b"\x8a\x80\x00\x44\x88\xcc") + self.assertFrameReceived( + server, + Frame(OP_PONG, b""), + ) + + def test_client_sends_pong_with_data(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x44\x88\xcc"): + client.send_pong(b"\x22\x66\xaa\xee") + self.assertEqual( + client.data_to_send(), [b"\x8a\x84\x00\x44\x88\xcc\x22\x22\x22\x22"] + ) + + def test_server_sends_pong_with_data(self): + server = Protocol(SERVER) + server.send_pong(b"\x22\x66\xaa\xee") + self.assertEqual(server.data_to_send(), [b"\x8a\x04\x22\x66\xaa\xee"]) + + def test_client_receives_pong_with_data(self): + client = Protocol(CLIENT) + client.receive_data(b"\x8a\x04\x22\x66\xaa\xee") + self.assertFrameReceived( + client, + Frame(OP_PONG, b"\x22\x66\xaa\xee"), + ) + + def test_server_receives_pong_with_data(self): + server = Protocol(SERVER) + server.receive_data(b"\x8a\x84\x00\x44\x88\xcc\x22\x22\x22\x22") + self.assertFrameReceived( + server, + Frame(OP_PONG, b"\x22\x66\xaa\xee"), + ) + + def test_client_sends_fragmented_pong_frame(self): + client = Protocol(CLIENT) + # This is only possible through a private API. + with self.assertRaises(ProtocolError) as raised: + client.send_frame(Frame(OP_PONG, b"", fin=False)) + self.assertEqual(str(raised.exception), "fragmented control frame") + + def test_server_sends_fragmented_pong_frame(self): + server = Protocol(SERVER) + # This is only possible through a private API. + with self.assertRaises(ProtocolError) as raised: + server.send_frame(Frame(OP_PONG, b"", fin=False)) + self.assertEqual(str(raised.exception), "fragmented control frame") + + def test_client_receives_fragmented_pong_frame(self): + client = Protocol(CLIENT) + client.receive_data(b"\x0a\x00") + self.assertIsInstance(client.parser_exc, ProtocolError) + self.assertEqual(str(client.parser_exc), "fragmented control frame") + self.assertConnectionFailing( + client, CloseCode.PROTOCOL_ERROR, "fragmented control frame" + ) + + def test_server_receives_fragmented_pong_frame(self): + server = Protocol(SERVER) + server.receive_data(b"\x0a\x80\x3c\x3c\x3c\x3c") + self.assertIsInstance(server.parser_exc, ProtocolError) + self.assertEqual(str(server.parser_exc), "fragmented control frame") + self.assertConnectionFailing( + server, CloseCode.PROTOCOL_ERROR, "fragmented control frame" + ) + + def test_client_sends_pong_after_sending_close(self): + client = Protocol(CLIENT) + with self.enforce_mask(b"\x00\x00\x00\x00"): + client.send_close(CloseCode.GOING_AWAY) + self.assertEqual(client.data_to_send(), [b"\x88\x82\x00\x00\x00\x00\x03\xe9"]) + # websockets doesn't support sending a Pong frame after a Close frame. + with self.assertRaises(InvalidState): + client.send_pong(b"") + + def test_server_sends_pong_after_sending_close(self): + server = Protocol(SERVER) + server.send_close(CloseCode.NORMAL_CLOSURE) + self.assertEqual(server.data_to_send(), [b"\x88\x02\x03\xe8"]) + # websockets doesn't support sending a Pong frame after a Close frame. + with self.assertRaises(InvalidState): + server.send_pong(b"") + + def test_client_receives_pong_after_receiving_close(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x02\x03\xe8") + self.assertConnectionClosing(client, CloseCode.NORMAL_CLOSURE) + client.receive_data(b"\x8a\x04\x22\x66\xaa\xee") + self.assertFrameReceived(client, None) + self.assertFrameSent(client, None) + + def test_server_receives_pong_after_receiving_close(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe9") + self.assertConnectionClosing(server, CloseCode.GOING_AWAY) + server.receive_data(b"\x8a\x84\x00\x44\x88\xcc\x22\x22\x22\x22") + self.assertFrameReceived(server, None) + self.assertFrameSent(server, None) + + +class FailTests(ProtocolTestCase): + """ + Test failing the connection. + + See 7.1.7. Fail the WebSocket Connection in RFC 6544. + + """ + + def test_client_stops_processing_frames_after_fail(self): + client = Protocol(CLIENT) + client.fail(CloseCode.PROTOCOL_ERROR) + self.assertConnectionFailing(client, CloseCode.PROTOCOL_ERROR) + client.receive_data(b"\x88\x02\x03\xea") + self.assertFrameReceived(client, None) + + def test_server_stops_processing_frames_after_fail(self): + server = Protocol(SERVER) + server.fail(CloseCode.PROTOCOL_ERROR) + self.assertConnectionFailing(server, CloseCode.PROTOCOL_ERROR) + server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xea") + self.assertFrameReceived(server, None) + + +class FragmentationTests(ProtocolTestCase): + """ + Test message fragmentation. + + See 5.4. Fragmentation in RFC 6544. + + """ + + def test_client_send_ping_pong_in_fragmented_message(self): + client = Protocol(CLIENT) + client.send_text(b"Spam", fin=False) + self.assertFrameSent(client, Frame(OP_TEXT, b"Spam", fin=False)) + client.send_ping(b"Ping") + self.assertFrameSent(client, Frame(OP_PING, b"Ping")) + client.send_continuation(b"Ham", fin=False) + self.assertFrameSent(client, Frame(OP_CONT, b"Ham", fin=False)) + client.send_pong(b"Pong") + self.assertFrameSent(client, Frame(OP_PONG, b"Pong")) + client.send_continuation(b"Eggs", fin=True) + self.assertFrameSent(client, Frame(OP_CONT, b"Eggs")) + + def test_server_send_ping_pong_in_fragmented_message(self): + server = Protocol(SERVER) + server.send_text(b"Spam", fin=False) + self.assertFrameSent(server, Frame(OP_TEXT, b"Spam", fin=False)) + server.send_ping(b"Ping") + self.assertFrameSent(server, Frame(OP_PING, b"Ping")) + server.send_continuation(b"Ham", fin=False) + self.assertFrameSent(server, Frame(OP_CONT, b"Ham", fin=False)) + server.send_pong(b"Pong") + self.assertFrameSent(server, Frame(OP_PONG, b"Pong")) + server.send_continuation(b"Eggs", fin=True) + self.assertFrameSent(server, Frame(OP_CONT, b"Eggs")) + + def test_client_receive_ping_pong_in_fragmented_message(self): + client = Protocol(CLIENT) + client.receive_data(b"\x01\x04Spam") + self.assertFrameReceived( + client, + Frame(OP_TEXT, b"Spam", fin=False), + ) + client.receive_data(b"\x89\x04Ping") + self.assertFrameReceived( + client, + Frame(OP_PING, b"Ping"), + ) + self.assertFrameSent( + client, + Frame(OP_PONG, b"Ping"), + ) + client.receive_data(b"\x00\x03Ham") + self.assertFrameReceived( + client, + Frame(OP_CONT, b"Ham", fin=False), + ) + client.receive_data(b"\x8a\x04Pong") + self.assertFrameReceived( + client, + Frame(OP_PONG, b"Pong"), + ) + client.receive_data(b"\x80\x04Eggs") + self.assertFrameReceived( + client, + Frame(OP_CONT, b"Eggs"), + ) + + def test_server_receive_ping_pong_in_fragmented_message(self): + server = Protocol(SERVER) + server.receive_data(b"\x01\x84\x00\x00\x00\x00Spam") + self.assertFrameReceived( + server, + Frame(OP_TEXT, b"Spam", fin=False), + ) + server.receive_data(b"\x89\x84\x00\x00\x00\x00Ping") + self.assertFrameReceived( + server, + Frame(OP_PING, b"Ping"), + ) + self.assertFrameSent( + server, + Frame(OP_PONG, b"Ping"), + ) + server.receive_data(b"\x00\x83\x00\x00\x00\x00Ham") + self.assertFrameReceived( + server, + Frame(OP_CONT, b"Ham", fin=False), + ) + server.receive_data(b"\x8a\x84\x00\x00\x00\x00Pong") + self.assertFrameReceived( + server, + Frame(OP_PONG, b"Pong"), + ) + server.receive_data(b"\x80\x84\x00\x00\x00\x00Eggs") + self.assertFrameReceived( + server, + Frame(OP_CONT, b"Eggs"), + ) + + def test_client_send_close_in_fragmented_message(self): + client = Protocol(CLIENT) + client.send_text(b"Spam", fin=False) + self.assertFrameSent(client, Frame(OP_TEXT, b"Spam", fin=False)) + # The spec says: "An endpoint MUST be capable of handling control + # frames in the middle of a fragmented message." However, since the + # endpoint must not send a data frame after a close frame, a close + # frame can't be "in the middle" of a fragmented message. + with self.assertRaises(ProtocolError) as raised: + client.send_close(CloseCode.GOING_AWAY) + self.assertEqual(str(raised.exception), "expected a continuation frame") + client.send_continuation(b"Eggs", fin=True) + + def test_server_send_close_in_fragmented_message(self): + server = Protocol(CLIENT) + server.send_text(b"Spam", fin=False) + self.assertFrameSent(server, Frame(OP_TEXT, b"Spam", fin=False)) + # The spec says: "An endpoint MUST be capable of handling control + # frames in the middle of a fragmented message." However, since the + # endpoint must not send a data frame after a close frame, a close + # frame can't be "in the middle" of a fragmented message. + with self.assertRaises(ProtocolError) as raised: + server.send_close(CloseCode.NORMAL_CLOSURE) + self.assertEqual(str(raised.exception), "expected a continuation frame") + + def test_client_receive_close_in_fragmented_message(self): + client = Protocol(CLIENT) + client.receive_data(b"\x01\x04Spam") + self.assertFrameReceived( + client, + Frame(OP_TEXT, b"Spam", fin=False), + ) + # The spec says: "An endpoint MUST be capable of handling control + # frames in the middle of a fragmented message." However, since the + # endpoint must not send a data frame after a close frame, a close + # frame can't be "in the middle" of a fragmented message. + client.receive_data(b"\x88\x02\x03\xe8") + self.assertIsInstance(client.parser_exc, ProtocolError) + self.assertEqual(str(client.parser_exc), "incomplete fragmented message") + self.assertConnectionFailing( + client, CloseCode.PROTOCOL_ERROR, "incomplete fragmented message" + ) + + def test_server_receive_close_in_fragmented_message(self): + server = Protocol(SERVER) + server.receive_data(b"\x01\x84\x00\x00\x00\x00Spam") + self.assertFrameReceived( + server, + Frame(OP_TEXT, b"Spam", fin=False), + ) + # The spec says: "An endpoint MUST be capable of handling control + # frames in the middle of a fragmented message." However, since the + # endpoint must not send a data frame after a close frame, a close + # frame can't be "in the middle" of a fragmented message. + server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe9") + self.assertIsInstance(server.parser_exc, ProtocolError) + self.assertEqual(str(server.parser_exc), "incomplete fragmented message") + self.assertConnectionFailing( + server, CloseCode.PROTOCOL_ERROR, "incomplete fragmented message" + ) + + +class EOFTests(ProtocolTestCase): + """ + Test half-closes on connection termination. + + """ + + def test_client_receives_eof(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x00") + self.assertConnectionClosing(client) + client.receive_eof() + self.assertIs(client.state, CLOSED) + + def test_server_receives_eof(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c") + self.assertConnectionClosing(server) + server.receive_eof() + self.assertIs(server.state, CLOSED) + + def test_client_receives_eof_between_frames(self): + client = Protocol(CLIENT) + client.receive_eof() + self.assertIsInstance(client.parser_exc, EOFError) + self.assertEqual(str(client.parser_exc), "unexpected end of stream") + self.assertIs(client.state, CLOSED) + + def test_server_receives_eof_between_frames(self): + server = Protocol(SERVER) + server.receive_eof() + self.assertIsInstance(server.parser_exc, EOFError) + self.assertEqual(str(server.parser_exc), "unexpected end of stream") + self.assertIs(server.state, CLOSED) + + def test_client_receives_eof_inside_frame(self): + client = Protocol(CLIENT) + client.receive_data(b"\x81") + client.receive_eof() + self.assertIsInstance(client.parser_exc, EOFError) + self.assertEqual( + str(client.parser_exc), + "stream ends after 1 bytes, expected 2 bytes", + ) + self.assertIs(client.state, CLOSED) + + def test_server_receives_eof_inside_frame(self): + server = Protocol(SERVER) + server.receive_data(b"\x81") + server.receive_eof() + self.assertIsInstance(server.parser_exc, EOFError) + self.assertEqual( + str(server.parser_exc), + "stream ends after 1 bytes, expected 2 bytes", + ) + self.assertIs(server.state, CLOSED) + + def test_client_receives_data_after_exception(self): + client = Protocol(CLIENT) + client.receive_data(b"\xff\xff") + self.assertConnectionFailing(client, CloseCode.PROTOCOL_ERROR, "invalid opcode") + client.receive_data(b"\x00\x00") + self.assertFrameSent(client, None) + + def test_server_receives_data_after_exception(self): + server = Protocol(SERVER) + server.receive_data(b"\xff\xff") + self.assertConnectionFailing(server, CloseCode.PROTOCOL_ERROR, "invalid opcode") + server.receive_data(b"\x00\x00") + self.assertFrameSent(server, None) + + def test_client_receives_eof_after_exception(self): + client = Protocol(CLIENT) + client.receive_data(b"\xff\xff") + self.assertConnectionFailing(client, CloseCode.PROTOCOL_ERROR, "invalid opcode") + client.receive_eof() + self.assertFrameSent(client, None, eof=True) + + def test_server_receives_eof_after_exception(self): + server = Protocol(SERVER) + server.receive_data(b"\xff\xff") + self.assertConnectionFailing(server, CloseCode.PROTOCOL_ERROR, "invalid opcode") + server.receive_eof() + self.assertFrameSent(server, None) + + def test_client_receives_data_and_eof_after_exception(self): + client = Protocol(CLIENT) + client.receive_data(b"\xff\xff") + self.assertConnectionFailing(client, CloseCode.PROTOCOL_ERROR, "invalid opcode") + client.receive_data(b"\x00\x00") + client.receive_eof() + self.assertFrameSent(client, None, eof=True) + + def test_server_receives_data_and_eof_after_exception(self): + server = Protocol(SERVER) + server.receive_data(b"\xff\xff") + self.assertConnectionFailing(server, CloseCode.PROTOCOL_ERROR, "invalid opcode") + server.receive_data(b"\x00\x00") + server.receive_eof() + self.assertFrameSent(server, None) + + def test_client_receives_data_after_eof(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x00") + self.assertConnectionClosing(client) + client.receive_eof() + with self.assertRaises(EOFError) as raised: + client.receive_data(b"\x88\x00") + self.assertEqual(str(raised.exception), "stream ended") + + def test_server_receives_data_after_eof(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c") + self.assertConnectionClosing(server) + server.receive_eof() + with self.assertRaises(EOFError) as raised: + server.receive_data(b"\x88\x80\x00\x00\x00\x00") + self.assertEqual(str(raised.exception), "stream ended") + + def test_client_receives_eof_after_eof(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x00") + self.assertConnectionClosing(client) + client.receive_eof() + with self.assertRaises(EOFError) as raised: + client.receive_eof() + self.assertEqual(str(raised.exception), "stream ended") + + def test_server_receives_eof_after_eof(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c") + self.assertConnectionClosing(server) + server.receive_eof() + with self.assertRaises(EOFError) as raised: + server.receive_eof() + self.assertEqual(str(raised.exception), "stream ended") + + +class TCPCloseTests(ProtocolTestCase): + """ + Test expectation of TCP close on connection termination. + + """ + + def test_client_default(self): + client = Protocol(CLIENT) + self.assertFalse(client.close_expected()) + + def test_server_default(self): + server = Protocol(SERVER) + self.assertFalse(server.close_expected()) + + def test_client_sends_close(self): + client = Protocol(CLIENT) + client.send_close() + self.assertTrue(client.close_expected()) + + def test_server_sends_close(self): + server = Protocol(SERVER) + server.send_close() + self.assertTrue(server.close_expected()) + + def test_client_receives_close(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x00") + self.assertTrue(client.close_expected()) + + def test_client_receives_close_then_eof(self): + client = Protocol(CLIENT) + client.receive_data(b"\x88\x00") + client.receive_eof() + self.assertFalse(client.close_expected()) + + def test_server_receives_close_then_eof(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c") + server.receive_eof() + self.assertFalse(server.close_expected()) + + def test_server_receives_close(self): + server = Protocol(SERVER) + server.receive_data(b"\x88\x80\x3c\x3c\x3c\x3c") + self.assertTrue(server.close_expected()) + + def test_client_fails_connection(self): + client = Protocol(CLIENT) + client.fail(CloseCode.PROTOCOL_ERROR) + self.assertTrue(client.close_expected()) + + def test_server_fails_connection(self): + server = Protocol(SERVER) + server.fail(CloseCode.PROTOCOL_ERROR) + self.assertTrue(server.close_expected()) + + +class ConnectionClosedTests(ProtocolTestCase): + """ + Test connection closed exception. + + """ + + def test_client_sends_close_then_receives_close(self): + # Client-initiated close handshake on the client side complete. + client = Protocol(CLIENT) + client.send_close(CloseCode.NORMAL_CLOSURE, "") + client.receive_data(b"\x88\x02\x03\xe8") + client.receive_eof() + exc = client.close_exc + self.assertIsInstance(exc, ConnectionClosedOK) + self.assertEqual(exc.rcvd, Close(CloseCode.NORMAL_CLOSURE, "")) + self.assertEqual(exc.sent, Close(CloseCode.NORMAL_CLOSURE, "")) + self.assertFalse(exc.rcvd_then_sent) + + def test_server_sends_close_then_receives_close(self): + # Server-initiated close handshake on the server side complete. + server = Protocol(SERVER) + server.send_close(CloseCode.NORMAL_CLOSURE, "") + server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe8") + server.receive_eof() + exc = server.close_exc + self.assertIsInstance(exc, ConnectionClosedOK) + self.assertEqual(exc.rcvd, Close(CloseCode.NORMAL_CLOSURE, "")) + self.assertEqual(exc.sent, Close(CloseCode.NORMAL_CLOSURE, "")) + self.assertFalse(exc.rcvd_then_sent) + + def test_client_receives_close_then_sends_close(self): + # Server-initiated close handshake on the client side complete. + client = Protocol(CLIENT) + client.receive_data(b"\x88\x02\x03\xe8") + client.receive_eof() + exc = client.close_exc + self.assertIsInstance(exc, ConnectionClosedOK) + self.assertEqual(exc.rcvd, Close(CloseCode.NORMAL_CLOSURE, "")) + self.assertEqual(exc.sent, Close(CloseCode.NORMAL_CLOSURE, "")) + self.assertTrue(exc.rcvd_then_sent) + + def test_server_receives_close_then_sends_close(self): + # Client-initiated close handshake on the server side complete. + server = Protocol(SERVER) + server.receive_data(b"\x88\x82\x00\x00\x00\x00\x03\xe8") + server.receive_eof() + exc = server.close_exc + self.assertIsInstance(exc, ConnectionClosedOK) + self.assertEqual(exc.rcvd, Close(CloseCode.NORMAL_CLOSURE, "")) + self.assertEqual(exc.sent, Close(CloseCode.NORMAL_CLOSURE, "")) + self.assertTrue(exc.rcvd_then_sent) + + def test_client_sends_close_then_receives_eof(self): + # Client-initiated close handshake on the client side times out. + client = Protocol(CLIENT) + client.send_close(CloseCode.NORMAL_CLOSURE, "") + client.receive_eof() + exc = client.close_exc + self.assertIsInstance(exc, ConnectionClosedError) + self.assertIsNone(exc.rcvd) + self.assertEqual(exc.sent, Close(CloseCode.NORMAL_CLOSURE, "")) + self.assertIsNone(exc.rcvd_then_sent) + + def test_server_sends_close_then_receives_eof(self): + # Server-initiated close handshake on the server side times out. + server = Protocol(SERVER) + server.send_close(CloseCode.NORMAL_CLOSURE, "") + server.receive_eof() + exc = server.close_exc + self.assertIsInstance(exc, ConnectionClosedError) + self.assertIsNone(exc.rcvd) + self.assertEqual(exc.sent, Close(CloseCode.NORMAL_CLOSURE, "")) + self.assertIsNone(exc.rcvd_then_sent) + + def test_client_receives_eof(self): + # Server-initiated close handshake on the client side times out. + client = Protocol(CLIENT) + client.receive_eof() + exc = client.close_exc + self.assertIsInstance(exc, ConnectionClosedError) + self.assertIsNone(exc.rcvd) + self.assertIsNone(exc.sent) + self.assertIsNone(exc.rcvd_then_sent) + + def test_server_receives_eof(self): + # Client-initiated close handshake on the server side times out. + server = Protocol(SERVER) + server.receive_eof() + exc = server.close_exc + self.assertIsInstance(exc, ConnectionClosedError) + self.assertIsNone(exc.rcvd) + self.assertIsNone(exc.sent) + self.assertIsNone(exc.rcvd_then_sent) + + +class ErrorTests(ProtocolTestCase): + """ + Test other error cases. + + """ + + def test_client_hits_internal_error_reading_frame(self): + client = Protocol(CLIENT) + # This isn't supposed to happen, so we're simulating it. + with unittest.mock.patch("struct.unpack", side_effect=RuntimeError("BOOM")): + client.receive_data(b"\x81\x00") + self.assertIsInstance(client.parser_exc, RuntimeError) + self.assertEqual(str(client.parser_exc), "BOOM") + self.assertConnectionFailing(client, CloseCode.INTERNAL_ERROR, "") + + def test_server_hits_internal_error_reading_frame(self): + server = Protocol(SERVER) + # This isn't supposed to happen, so we're simulating it. + with unittest.mock.patch("struct.unpack", side_effect=RuntimeError("BOOM")): + server.receive_data(b"\x81\x80\x00\x00\x00\x00") + self.assertIsInstance(server.parser_exc, RuntimeError) + self.assertEqual(str(server.parser_exc), "BOOM") + self.assertConnectionFailing(server, CloseCode.INTERNAL_ERROR, "") + + +class ExtensionsTests(ProtocolTestCase): + """ + Test how extensions affect frames. + + """ + + def test_client_extension_encodes_frame(self): + client = Protocol(CLIENT) + client.extensions = [Rsv2Extension()] + with self.enforce_mask(b"\x00\x44\x88\xcc"): + client.send_ping(b"") + self.assertEqual(client.data_to_send(), [b"\xa9\x80\x00\x44\x88\xcc"]) + + def test_server_extension_encodes_frame(self): + server = Protocol(SERVER) + server.extensions = [Rsv2Extension()] + server.send_ping(b"") + self.assertEqual(server.data_to_send(), [b"\xa9\x00"]) + + def test_client_extension_decodes_frame(self): + client = Protocol(CLIENT) + client.extensions = [Rsv2Extension()] + client.receive_data(b"\xaa\x00") + self.assertEqual(client.events_received(), [Frame(OP_PONG, b"")]) + + def test_server_extension_decodes_frame(self): + server = Protocol(SERVER) + server.extensions = [Rsv2Extension()] + server.receive_data(b"\xaa\x80\x00\x44\x88\xcc") + self.assertEqual(server.events_received(), [Frame(OP_PONG, b"")]) + + +class MiscTests(unittest.TestCase): + def test_client_default_logger(self): + client = Protocol(CLIENT) + logger = logging.getLogger("websockets.client") + self.assertIs(client.logger, logger) + + def test_server_default_logger(self): + server = Protocol(SERVER) + logger = logging.getLogger("websockets.server") + self.assertIs(server.logger, logger) + + def test_client_custom_logger(self): + logger = logging.getLogger("test") + client = Protocol(CLIENT, logger=logger) + self.assertIs(client.logger, logger) + + def test_server_custom_logger(self): + logger = logging.getLogger("test") + server = Protocol(SERVER, logger=logger) + self.assertIs(server.logger, logger) diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/test_server.py b/tests/wpt/tests/tools/third_party/websockets/tests/test_server.py new file mode 100644 index 00000000000..b6f5e356819 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/test_server.py @@ -0,0 +1,686 @@ +import http +import logging +import unittest +import unittest.mock + +from websockets.datastructures import Headers +from websockets.exceptions import ( + InvalidHeader, + InvalidOrigin, + InvalidUpgrade, + NegotiationError, +) +from websockets.frames import OP_TEXT, Frame +from websockets.http11 import Request, Response +from websockets.protocol import CONNECTING, OPEN +from websockets.server import * + +from .extensions.utils import ( + OpExtension, + Rsv2Extension, + ServerOpExtensionFactory, + ServerRsv2ExtensionFactory, +) +from .test_utils import ACCEPT, KEY +from .utils import DATE, DeprecationTestCase + + +class ConnectTests(unittest.TestCase): + def test_receive_connect(self): + server = ServerProtocol() + server.receive_data( + ( + f"GET /test HTTP/1.1\r\n" + f"Host: example.com\r\n" + f"Upgrade: websocket\r\n" + f"Connection: Upgrade\r\n" + f"Sec-WebSocket-Key: {KEY}\r\n" + f"Sec-WebSocket-Version: 13\r\n" + f"\r\n" + ).encode(), + ) + [request] = server.events_received() + self.assertIsInstance(request, Request) + self.assertEqual(server.data_to_send(), []) + self.assertFalse(server.close_expected()) + + def test_connect_request(self): + server = ServerProtocol() + server.receive_data( + ( + f"GET /test HTTP/1.1\r\n" + f"Host: example.com\r\n" + f"Upgrade: websocket\r\n" + f"Connection: Upgrade\r\n" + f"Sec-WebSocket-Key: {KEY}\r\n" + f"Sec-WebSocket-Version: 13\r\n" + f"\r\n" + ).encode(), + ) + [request] = server.events_received() + self.assertEqual(request.path, "/test") + self.assertEqual( + request.headers, + Headers( + { + "Host": "example.com", + "Upgrade": "websocket", + "Connection": "Upgrade", + "Sec-WebSocket-Key": KEY, + "Sec-WebSocket-Version": "13", + } + ), + ) + + def test_no_request(self): + server = ServerProtocol() + server.receive_eof() + self.assertEqual(server.events_received(), []) + + def test_partial_request(self): + server = ServerProtocol() + server.receive_data(b"GET /test HTTP/1.1\r\n") + server.receive_eof() + self.assertEqual(server.events_received(), []) + + def test_random_request(self): + server = ServerProtocol() + server.receive_data(b"HELO relay.invalid\r\n") + server.receive_data(b"MAIL FROM: <alice@invalid>\r\n") + server.receive_data(b"RCPT TO: <bob@invalid>\r\n") + self.assertEqual(server.events_received(), []) + + +class AcceptRejectTests(unittest.TestCase): + def make_request(self): + return Request( + path="/test", + headers=Headers( + { + "Host": "example.com", + "Upgrade": "websocket", + "Connection": "Upgrade", + "Sec-WebSocket-Key": KEY, + "Sec-WebSocket-Version": "13", + } + ), + ) + + def test_send_accept(self): + server = ServerProtocol() + with unittest.mock.patch("email.utils.formatdate", return_value=DATE): + response = server.accept(self.make_request()) + self.assertIsInstance(response, Response) + server.send_response(response) + self.assertEqual( + server.data_to_send(), + [ + f"HTTP/1.1 101 Switching Protocols\r\n" + f"Date: {DATE}\r\n" + f"Upgrade: websocket\r\n" + f"Connection: Upgrade\r\n" + f"Sec-WebSocket-Accept: {ACCEPT}\r\n" + f"\r\n".encode() + ], + ) + self.assertFalse(server.close_expected()) + self.assertEqual(server.state, OPEN) + + def test_send_reject(self): + server = ServerProtocol() + with unittest.mock.patch("email.utils.formatdate", return_value=DATE): + response = server.reject(http.HTTPStatus.NOT_FOUND, "Sorry folks.\n") + self.assertIsInstance(response, Response) + server.send_response(response) + self.assertEqual( + server.data_to_send(), + [ + f"HTTP/1.1 404 Not Found\r\n" + f"Date: {DATE}\r\n" + f"Connection: close\r\n" + f"Content-Length: 13\r\n" + f"Content-Type: text/plain; charset=utf-8\r\n" + f"\r\n" + f"Sorry folks.\n".encode(), + b"", + ], + ) + self.assertTrue(server.close_expected()) + self.assertEqual(server.state, CONNECTING) + + def test_accept_response(self): + server = ServerProtocol() + with unittest.mock.patch("email.utils.formatdate", return_value=DATE): + response = server.accept(self.make_request()) + self.assertIsInstance(response, Response) + self.assertEqual(response.status_code, 101) + self.assertEqual(response.reason_phrase, "Switching Protocols") + self.assertEqual( + response.headers, + Headers( + { + "Date": DATE, + "Upgrade": "websocket", + "Connection": "Upgrade", + "Sec-WebSocket-Accept": ACCEPT, + } + ), + ) + self.assertIsNone(response.body) + + def test_reject_response(self): + server = ServerProtocol() + with unittest.mock.patch("email.utils.formatdate", return_value=DATE): + response = server.reject(http.HTTPStatus.NOT_FOUND, "Sorry folks.\n") + self.assertIsInstance(response, Response) + self.assertEqual(response.status_code, 404) + self.assertEqual(response.reason_phrase, "Not Found") + self.assertEqual( + response.headers, + Headers( + { + "Date": DATE, + "Connection": "close", + "Content-Length": "13", + "Content-Type": "text/plain; charset=utf-8", + } + ), + ) + self.assertEqual(response.body, b"Sorry folks.\n") + + def test_reject_response_supports_int_status(self): + server = ServerProtocol() + response = server.reject(404, "Sorry folks.\n") + self.assertEqual(response.status_code, 404) + self.assertEqual(response.reason_phrase, "Not Found") + + def test_basic(self): + server = ServerProtocol() + request = self.make_request() + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + + def test_unexpected_exception(self): + server = ServerProtocol() + request = self.make_request() + with unittest.mock.patch( + "websockets.server.ServerProtocol.process_request", + side_effect=Exception("BOOM"), + ): + response = server.accept(request) + + self.assertEqual(response.status_code, 500) + with self.assertRaises(Exception) as raised: + raise server.handshake_exc + self.assertEqual(str(raised.exception), "BOOM") + + def test_missing_connection(self): + server = ServerProtocol() + request = self.make_request() + del request.headers["Connection"] + response = server.accept(request) + + self.assertEqual(response.status_code, 426) + self.assertEqual(response.headers["Upgrade"], "websocket") + with self.assertRaises(InvalidUpgrade) as raised: + raise server.handshake_exc + self.assertEqual(str(raised.exception), "missing Connection header") + + def test_invalid_connection(self): + server = ServerProtocol() + request = self.make_request() + del request.headers["Connection"] + request.headers["Connection"] = "close" + response = server.accept(request) + + self.assertEqual(response.status_code, 426) + self.assertEqual(response.headers["Upgrade"], "websocket") + with self.assertRaises(InvalidUpgrade) as raised: + raise server.handshake_exc + self.assertEqual(str(raised.exception), "invalid Connection header: close") + + def test_missing_upgrade(self): + server = ServerProtocol() + request = self.make_request() + del request.headers["Upgrade"] + response = server.accept(request) + + self.assertEqual(response.status_code, 426) + self.assertEqual(response.headers["Upgrade"], "websocket") + with self.assertRaises(InvalidUpgrade) as raised: + raise server.handshake_exc + self.assertEqual(str(raised.exception), "missing Upgrade header") + + def test_invalid_upgrade(self): + server = ServerProtocol() + request = self.make_request() + del request.headers["Upgrade"] + request.headers["Upgrade"] = "h2c" + response = server.accept(request) + + self.assertEqual(response.status_code, 426) + self.assertEqual(response.headers["Upgrade"], "websocket") + with self.assertRaises(InvalidUpgrade) as raised: + raise server.handshake_exc + self.assertEqual(str(raised.exception), "invalid Upgrade header: h2c") + + def test_missing_key(self): + server = ServerProtocol() + request = self.make_request() + del request.headers["Sec-WebSocket-Key"] + response = server.accept(request) + + self.assertEqual(response.status_code, 400) + with self.assertRaises(InvalidHeader) as raised: + raise server.handshake_exc + self.assertEqual(str(raised.exception), "missing Sec-WebSocket-Key header") + + def test_multiple_key(self): + server = ServerProtocol() + request = self.make_request() + request.headers["Sec-WebSocket-Key"] = KEY + response = server.accept(request) + + self.assertEqual(response.status_code, 400) + with self.assertRaises(InvalidHeader) as raised: + raise server.handshake_exc + self.assertEqual( + str(raised.exception), + "invalid Sec-WebSocket-Key header: " + "more than one Sec-WebSocket-Key header found", + ) + + def test_invalid_key(self): + server = ServerProtocol() + request = self.make_request() + del request.headers["Sec-WebSocket-Key"] + request.headers["Sec-WebSocket-Key"] = "not Base64 data!" + response = server.accept(request) + + self.assertEqual(response.status_code, 400) + with self.assertRaises(InvalidHeader) as raised: + raise server.handshake_exc + self.assertEqual( + str(raised.exception), "invalid Sec-WebSocket-Key header: not Base64 data!" + ) + + def test_truncated_key(self): + server = ServerProtocol() + request = self.make_request() + del request.headers["Sec-WebSocket-Key"] + request.headers["Sec-WebSocket-Key"] = KEY[ + :16 + ] # 12 bytes instead of 16, Base64-encoded + response = server.accept(request) + + self.assertEqual(response.status_code, 400) + with self.assertRaises(InvalidHeader) as raised: + raise server.handshake_exc + self.assertEqual( + str(raised.exception), f"invalid Sec-WebSocket-Key header: {KEY[:16]}" + ) + + def test_missing_version(self): + server = ServerProtocol() + request = self.make_request() + del request.headers["Sec-WebSocket-Version"] + response = server.accept(request) + + self.assertEqual(response.status_code, 400) + with self.assertRaises(InvalidHeader) as raised: + raise server.handshake_exc + self.assertEqual(str(raised.exception), "missing Sec-WebSocket-Version header") + + def test_multiple_version(self): + server = ServerProtocol() + request = self.make_request() + request.headers["Sec-WebSocket-Version"] = "11" + response = server.accept(request) + + self.assertEqual(response.status_code, 400) + with self.assertRaises(InvalidHeader) as raised: + raise server.handshake_exc + self.assertEqual( + str(raised.exception), + "invalid Sec-WebSocket-Version header: " + "more than one Sec-WebSocket-Version header found", + ) + + def test_invalid_version(self): + server = ServerProtocol() + request = self.make_request() + del request.headers["Sec-WebSocket-Version"] + request.headers["Sec-WebSocket-Version"] = "11" + response = server.accept(request) + + self.assertEqual(response.status_code, 400) + with self.assertRaises(InvalidHeader) as raised: + raise server.handshake_exc + self.assertEqual( + str(raised.exception), "invalid Sec-WebSocket-Version header: 11" + ) + + def test_no_origin(self): + server = ServerProtocol(origins=["https://example.com"]) + request = self.make_request() + response = server.accept(request) + + self.assertEqual(response.status_code, 403) + with self.assertRaises(InvalidOrigin) as raised: + raise server.handshake_exc + self.assertEqual(str(raised.exception), "missing Origin header") + + def test_origin(self): + server = ServerProtocol(origins=["https://example.com"]) + request = self.make_request() + request.headers["Origin"] = "https://example.com" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertEqual(server.origin, "https://example.com") + + def test_unexpected_origin(self): + server = ServerProtocol(origins=["https://example.com"]) + request = self.make_request() + request.headers["Origin"] = "https://other.example.com" + response = server.accept(request) + + self.assertEqual(response.status_code, 403) + with self.assertRaises(InvalidOrigin) as raised: + raise server.handshake_exc + self.assertEqual( + str(raised.exception), "invalid Origin header: https://other.example.com" + ) + + def test_multiple_origin(self): + server = ServerProtocol( + origins=["https://example.com", "https://other.example.com"] + ) + request = self.make_request() + request.headers["Origin"] = "https://example.com" + request.headers["Origin"] = "https://other.example.com" + response = server.accept(request) + + # This is prohibited by the HTTP specification, so the return code is + # 400 Bad Request rather than 403 Forbidden. + self.assertEqual(response.status_code, 400) + with self.assertRaises(InvalidHeader) as raised: + raise server.handshake_exc + self.assertEqual( + str(raised.exception), + "invalid Origin header: more than one Origin header found", + ) + + def test_supported_origin(self): + server = ServerProtocol( + origins=["https://example.com", "https://other.example.com"] + ) + request = self.make_request() + request.headers["Origin"] = "https://other.example.com" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertEqual(server.origin, "https://other.example.com") + + def test_unsupported_origin(self): + server = ServerProtocol( + origins=["https://example.com", "https://other.example.com"] + ) + request = self.make_request() + request.headers["Origin"] = "https://original.example.com" + response = server.accept(request) + + self.assertEqual(response.status_code, 403) + with self.assertRaises(InvalidOrigin) as raised: + raise server.handshake_exc + self.assertEqual( + str(raised.exception), "invalid Origin header: https://original.example.com" + ) + + def test_no_origin_accepted(self): + server = ServerProtocol(origins=[None]) + request = self.make_request() + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertIsNone(server.origin) + + def test_no_extensions(self): + server = ServerProtocol() + request = self.make_request() + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertNotIn("Sec-WebSocket-Extensions", response.headers) + self.assertEqual(server.extensions, []) + + def test_no_extension(self): + server = ServerProtocol(extensions=[ServerOpExtensionFactory()]) + request = self.make_request() + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertNotIn("Sec-WebSocket-Extensions", response.headers) + self.assertEqual(server.extensions, []) + + def test_extension(self): + server = ServerProtocol(extensions=[ServerOpExtensionFactory()]) + request = self.make_request() + request.headers["Sec-WebSocket-Extensions"] = "x-op; op" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertEqual(response.headers["Sec-WebSocket-Extensions"], "x-op; op") + self.assertEqual(server.extensions, [OpExtension()]) + + def test_unexpected_extension(self): + server = ServerProtocol() + request = self.make_request() + request.headers["Sec-WebSocket-Extensions"] = "x-op; op" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertNotIn("Sec-WebSocket-Extensions", response.headers) + self.assertEqual(server.extensions, []) + + def test_unsupported_extension(self): + server = ServerProtocol(extensions=[ServerRsv2ExtensionFactory()]) + request = self.make_request() + request.headers["Sec-WebSocket-Extensions"] = "x-op; op" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertNotIn("Sec-WebSocket-Extensions", response.headers) + self.assertEqual(server.extensions, []) + + def test_supported_extension_parameters(self): + server = ServerProtocol(extensions=[ServerOpExtensionFactory("this")]) + request = self.make_request() + request.headers["Sec-WebSocket-Extensions"] = "x-op; op=this" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertEqual(response.headers["Sec-WebSocket-Extensions"], "x-op; op=this") + self.assertEqual(server.extensions, [OpExtension("this")]) + + def test_unsupported_extension_parameters(self): + server = ServerProtocol(extensions=[ServerOpExtensionFactory("this")]) + request = self.make_request() + request.headers["Sec-WebSocket-Extensions"] = "x-op; op=that" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertNotIn("Sec-WebSocket-Extensions", response.headers) + self.assertEqual(server.extensions, []) + + def test_multiple_supported_extension_parameters(self): + server = ServerProtocol( + extensions=[ + ServerOpExtensionFactory("this"), + ServerOpExtensionFactory("that"), + ] + ) + request = self.make_request() + request.headers["Sec-WebSocket-Extensions"] = "x-op; op=that" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertEqual(response.headers["Sec-WebSocket-Extensions"], "x-op; op=that") + self.assertEqual(server.extensions, [OpExtension("that")]) + + def test_multiple_extensions(self): + server = ServerProtocol( + extensions=[ServerOpExtensionFactory(), ServerRsv2ExtensionFactory()] + ) + request = self.make_request() + request.headers["Sec-WebSocket-Extensions"] = "x-op; op" + request.headers["Sec-WebSocket-Extensions"] = "x-rsv2" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertEqual( + response.headers["Sec-WebSocket-Extensions"], "x-op; op, x-rsv2" + ) + self.assertEqual(server.extensions, [OpExtension(), Rsv2Extension()]) + + def test_multiple_extensions_order(self): + server = ServerProtocol( + extensions=[ServerOpExtensionFactory(), ServerRsv2ExtensionFactory()] + ) + request = self.make_request() + request.headers["Sec-WebSocket-Extensions"] = "x-rsv2" + request.headers["Sec-WebSocket-Extensions"] = "x-op; op" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertEqual( + response.headers["Sec-WebSocket-Extensions"], "x-rsv2, x-op; op" + ) + self.assertEqual(server.extensions, [Rsv2Extension(), OpExtension()]) + + def test_no_subprotocols(self): + server = ServerProtocol() + request = self.make_request() + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertNotIn("Sec-WebSocket-Protocol", response.headers) + self.assertIsNone(server.subprotocol) + + def test_no_subprotocol(self): + server = ServerProtocol(subprotocols=["chat"]) + request = self.make_request() + response = server.accept(request) + + self.assertEqual(response.status_code, 400) + with self.assertRaisesRegex( + NegotiationError, + r"missing subprotocol", + ): + raise server.handshake_exc + + def test_subprotocol(self): + server = ServerProtocol(subprotocols=["chat"]) + request = self.make_request() + request.headers["Sec-WebSocket-Protocol"] = "chat" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertEqual(response.headers["Sec-WebSocket-Protocol"], "chat") + self.assertEqual(server.subprotocol, "chat") + + def test_unexpected_subprotocol(self): + server = ServerProtocol() + request = self.make_request() + request.headers["Sec-WebSocket-Protocol"] = "chat" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertNotIn("Sec-WebSocket-Protocol", response.headers) + self.assertIsNone(server.subprotocol) + + def test_multiple_subprotocols(self): + server = ServerProtocol(subprotocols=["superchat", "chat"]) + request = self.make_request() + request.headers["Sec-WebSocket-Protocol"] = "chat" + request.headers["Sec-WebSocket-Protocol"] = "superchat" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertEqual(response.headers["Sec-WebSocket-Protocol"], "superchat") + self.assertEqual(server.subprotocol, "superchat") + + def test_supported_subprotocol(self): + server = ServerProtocol(subprotocols=["superchat", "chat"]) + request = self.make_request() + request.headers["Sec-WebSocket-Protocol"] = "chat" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertEqual(response.headers["Sec-WebSocket-Protocol"], "chat") + self.assertEqual(server.subprotocol, "chat") + + def test_unsupported_subprotocol(self): + server = ServerProtocol(subprotocols=["superchat", "chat"]) + request = self.make_request() + request.headers["Sec-WebSocket-Protocol"] = "otherchat" + response = server.accept(request) + + self.assertEqual(response.status_code, 400) + with self.assertRaisesRegex( + NegotiationError, + r"invalid subprotocol; expected one of superchat, chat", + ): + raise server.handshake_exc + + @staticmethod + def optional_chat(protocol, subprotocols): + if "chat" in subprotocols: + return "chat" + + def test_select_subprotocol(self): + server = ServerProtocol(select_subprotocol=self.optional_chat) + request = self.make_request() + request.headers["Sec-WebSocket-Protocol"] = "chat" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertEqual(response.headers["Sec-WebSocket-Protocol"], "chat") + self.assertEqual(server.subprotocol, "chat") + + def test_select_no_subprotocol(self): + server = ServerProtocol(select_subprotocol=self.optional_chat) + request = self.make_request() + request.headers["Sec-WebSocket-Protocol"] = "otherchat" + response = server.accept(request) + + self.assertEqual(response.status_code, 101) + self.assertNotIn("Sec-WebSocket-Protocol", response.headers) + self.assertIsNone(server.subprotocol) + + +class MiscTests(unittest.TestCase): + def test_bypass_handshake(self): + server = ServerProtocol(state=OPEN) + server.receive_data(b"\x81\x86\x00\x00\x00\x00Hello!") + [frame] = server.events_received() + self.assertEqual(frame, Frame(OP_TEXT, b"Hello!")) + + def test_custom_logger(self): + logger = logging.getLogger("test") + with self.assertLogs("test", logging.DEBUG) as logs: + ServerProtocol(logger=logger) + self.assertEqual(len(logs.records), 1) + + +class BackwardsCompatibilityTests(DeprecationTestCase): + def test_server_connection_class(self): + with self.assertDeprecationWarning( + "ServerConnection was renamed to ServerProtocol" + ): + from websockets.server import ServerConnection + + server = ServerConnection() + + self.assertIsInstance(server, ServerProtocol) diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/test_streams.py b/tests/wpt/tests/tools/third_party/websockets/tests/test_streams.py new file mode 100644 index 00000000000..fd7c66a0bdc --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/test_streams.py @@ -0,0 +1,198 @@ +from websockets.streams import StreamReader + +from .utils import GeneratorTestCase + + +class StreamReaderTests(GeneratorTestCase): + def setUp(self): + self.reader = StreamReader() + + def test_read_line(self): + self.reader.feed_data(b"spam\neggs\n") + + gen = self.reader.read_line(32) + line = self.assertGeneratorReturns(gen) + self.assertEqual(line, b"spam\n") + + gen = self.reader.read_line(32) + line = self.assertGeneratorReturns(gen) + self.assertEqual(line, b"eggs\n") + + def test_read_line_need_more_data(self): + self.reader.feed_data(b"spa") + + gen = self.reader.read_line(32) + self.assertGeneratorRunning(gen) + self.reader.feed_data(b"m\neg") + line = self.assertGeneratorReturns(gen) + self.assertEqual(line, b"spam\n") + + gen = self.reader.read_line(32) + self.assertGeneratorRunning(gen) + self.reader.feed_data(b"gs\n") + line = self.assertGeneratorReturns(gen) + self.assertEqual(line, b"eggs\n") + + def test_read_line_not_enough_data(self): + self.reader.feed_data(b"spa") + self.reader.feed_eof() + + gen = self.reader.read_line(32) + with self.assertRaises(EOFError) as raised: + next(gen) + self.assertEqual( + str(raised.exception), + "stream ends after 3 bytes, before end of line", + ) + + def test_read_line_too_long(self): + self.reader.feed_data(b"spam\neggs\n") + + gen = self.reader.read_line(2) + with self.assertRaises(RuntimeError) as raised: + next(gen) + self.assertEqual( + str(raised.exception), + "read 5 bytes, expected no more than 2 bytes", + ) + + def test_read_line_too_long_need_more_data(self): + self.reader.feed_data(b"spa") + + gen = self.reader.read_line(2) + with self.assertRaises(RuntimeError) as raised: + next(gen) + self.assertEqual( + str(raised.exception), + "read 3 bytes, expected no more than 2 bytes", + ) + + def test_read_exact(self): + self.reader.feed_data(b"spameggs") + + gen = self.reader.read_exact(4) + data = self.assertGeneratorReturns(gen) + self.assertEqual(data, b"spam") + + gen = self.reader.read_exact(4) + data = self.assertGeneratorReturns(gen) + self.assertEqual(data, b"eggs") + + def test_read_exact_need_more_data(self): + self.reader.feed_data(b"spa") + + gen = self.reader.read_exact(4) + self.assertGeneratorRunning(gen) + self.reader.feed_data(b"meg") + data = self.assertGeneratorReturns(gen) + self.assertEqual(data, b"spam") + + gen = self.reader.read_exact(4) + self.assertGeneratorRunning(gen) + self.reader.feed_data(b"gs") + data = self.assertGeneratorReturns(gen) + self.assertEqual(data, b"eggs") + + def test_read_exact_not_enough_data(self): + self.reader.feed_data(b"spa") + self.reader.feed_eof() + + gen = self.reader.read_exact(4) + with self.assertRaises(EOFError) as raised: + next(gen) + self.assertEqual( + str(raised.exception), + "stream ends after 3 bytes, expected 4 bytes", + ) + + def test_read_to_eof(self): + gen = self.reader.read_to_eof(32) + + self.reader.feed_data(b"spam") + self.assertGeneratorRunning(gen) + + self.reader.feed_eof() + data = self.assertGeneratorReturns(gen) + self.assertEqual(data, b"spam") + + def test_read_to_eof_at_eof(self): + self.reader.feed_eof() + + gen = self.reader.read_to_eof(32) + data = self.assertGeneratorReturns(gen) + self.assertEqual(data, b"") + + def test_read_to_eof_too_long(self): + gen = self.reader.read_to_eof(2) + + self.reader.feed_data(b"spam") + with self.assertRaises(RuntimeError) as raised: + next(gen) + self.assertEqual( + str(raised.exception), + "read 4 bytes, expected no more than 2 bytes", + ) + + def test_at_eof_after_feed_data(self): + gen = self.reader.at_eof() + self.assertGeneratorRunning(gen) + self.reader.feed_data(b"spam") + eof = self.assertGeneratorReturns(gen) + self.assertFalse(eof) + + def test_at_eof_after_feed_eof(self): + gen = self.reader.at_eof() + self.assertGeneratorRunning(gen) + self.reader.feed_eof() + eof = self.assertGeneratorReturns(gen) + self.assertTrue(eof) + + def test_feed_data_after_feed_data(self): + self.reader.feed_data(b"spam") + self.reader.feed_data(b"eggs") + + gen = self.reader.read_exact(8) + data = self.assertGeneratorReturns(gen) + self.assertEqual(data, b"spameggs") + gen = self.reader.at_eof() + self.assertGeneratorRunning(gen) + + def test_feed_eof_after_feed_data(self): + self.reader.feed_data(b"spam") + self.reader.feed_eof() + + gen = self.reader.read_exact(4) + data = self.assertGeneratorReturns(gen) + self.assertEqual(data, b"spam") + gen = self.reader.at_eof() + eof = self.assertGeneratorReturns(gen) + self.assertTrue(eof) + + def test_feed_data_after_feed_eof(self): + self.reader.feed_eof() + with self.assertRaises(EOFError) as raised: + self.reader.feed_data(b"spam") + self.assertEqual( + str(raised.exception), + "stream ended", + ) + + def test_feed_eof_after_feed_eof(self): + self.reader.feed_eof() + with self.assertRaises(EOFError) as raised: + self.reader.feed_eof() + self.assertEqual( + str(raised.exception), + "stream ended", + ) + + def test_discard(self): + gen = self.reader.read_to_eof(32) + + self.reader.feed_data(b"spam") + self.reader.discard() + self.assertGeneratorRunning(gen) + + self.reader.feed_eof() + data = self.assertGeneratorReturns(gen) + self.assertEqual(data, b"") diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/test_typing.py b/tests/wpt/tests/tools/third_party/websockets/tests/test_typing.py new file mode 100644 index 00000000000..202de840f35 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/test_typing.py @@ -0,0 +1 @@ +from websockets.typing import * diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/test_uri.py b/tests/wpt/tests/tools/third_party/websockets/tests/test_uri.py new file mode 100644 index 00000000000..8acc01c1871 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/test_uri.py @@ -0,0 +1,96 @@ +import unittest + +from websockets.exceptions import InvalidURI +from websockets.uri import * + + +VALID_URIS = [ + ( + "ws://localhost/", + WebSocketURI(False, "localhost", 80, "/", "", None, None), + ), + ( + "wss://localhost/", + WebSocketURI(True, "localhost", 443, "/", "", None, None), + ), + ( + "ws://localhost", + WebSocketURI(False, "localhost", 80, "", "", None, None), + ), + ( + "ws://localhost/path?query", + WebSocketURI(False, "localhost", 80, "/path", "query", None, None), + ), + ( + "ws://localhost/path;params", + WebSocketURI(False, "localhost", 80, "/path;params", "", None, None), + ), + ( + "WS://LOCALHOST/PATH?QUERY", + WebSocketURI(False, "localhost", 80, "/PATH", "QUERY", None, None), + ), + ( + "ws://user:pass@localhost/", + WebSocketURI(False, "localhost", 80, "/", "", "user", "pass"), + ), + ( + "ws://høst/", + WebSocketURI(False, "xn--hst-0na", 80, "/", "", None, None), + ), + ( + "ws://üser:påss@høst/πass?qùéry", + WebSocketURI( + False, + "xn--hst-0na", + 80, + "/%CF%80ass", + "q%C3%B9%C3%A9ry", + "%C3%BCser", + "p%C3%A5ss", + ), + ), +] + +INVALID_URIS = [ + "http://localhost/", + "https://localhost/", + "ws://localhost/path#fragment", + "ws://user@localhost/", + "ws:///path", +] + +RESOURCE_NAMES = [ + ("ws://localhost/", "/"), + ("ws://localhost", "/"), + ("ws://localhost/path?query", "/path?query"), + ("ws://høst/πass?qùéry", "/%CF%80ass?q%C3%B9%C3%A9ry"), +] + +USER_INFOS = [ + ("ws://localhost/", None), + ("ws://user:pass@localhost/", ("user", "pass")), + ("ws://üser:påss@høst/", ("%C3%BCser", "p%C3%A5ss")), +] + + +class URITests(unittest.TestCase): + def test_success(self): + for uri, parsed in VALID_URIS: + with self.subTest(uri=uri): + self.assertEqual(parse_uri(uri), parsed) + + def test_error(self): + for uri in INVALID_URIS: + with self.subTest(uri=uri): + with self.assertRaises(InvalidURI): + parse_uri(uri) + + def test_resource_name(self): + for uri, resource_name in RESOURCE_NAMES: + with self.subTest(uri=uri): + self.assertEqual(parse_uri(uri).resource_name, resource_name) + + def test_user_info(self): + for uri, user_info in USER_INFOS: + with self.subTest(uri=uri): + self.assertEqual(parse_uri(uri).user_info, user_info) diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/test_utils.py b/tests/wpt/tests/tools/third_party/websockets/tests/test_utils.py new file mode 100644 index 00000000000..678fcfe798e --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/test_utils.py @@ -0,0 +1,103 @@ +import base64 +import itertools +import platform +import unittest + +from websockets.utils import accept_key, apply_mask as py_apply_mask, generate_key + + +# Test vector from RFC 6455 +KEY = "dGhlIHNhbXBsZSBub25jZQ==" +ACCEPT = "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=" + + +class UtilsTests(unittest.TestCase): + def test_generate_key(self): + key = generate_key() + self.assertEqual(len(base64.b64decode(key.encode())), 16) + + def test_accept_key(self): + self.assertEqual(accept_key(KEY), ACCEPT) + + +class ApplyMaskTests(unittest.TestCase): + @staticmethod + def apply_mask(*args, **kwargs): + return py_apply_mask(*args, **kwargs) + + apply_mask_type_combos = list(itertools.product([bytes, bytearray], repeat=2)) + + apply_mask_test_values = [ + (b"", b"1234", b""), + (b"aBcDe", b"\x00\x00\x00\x00", b"aBcDe"), + (b"abcdABCD", b"1234", b"PPPPpppp"), + (b"abcdABCD" * 10, b"1234", b"PPPPpppp" * 10), + ] + + def test_apply_mask(self): + for data_type, mask_type in self.apply_mask_type_combos: + for data_in, mask, data_out in self.apply_mask_test_values: + data_in, mask = data_type(data_in), mask_type(mask) + + with self.subTest(data_in=data_in, mask=mask): + result = self.apply_mask(data_in, mask) + self.assertEqual(result, data_out) + + def test_apply_mask_memoryview(self): + for mask_type in [bytes, bytearray]: + for data_in, mask, data_out in self.apply_mask_test_values: + data_in, mask = memoryview(data_in), mask_type(mask) + + with self.subTest(data_in=data_in, mask=mask): + result = self.apply_mask(data_in, mask) + self.assertEqual(result, data_out) + + def test_apply_mask_non_contiguous_memoryview(self): + for mask_type in [bytes, bytearray]: + for data_in, mask, data_out in self.apply_mask_test_values: + data_in, mask = memoryview(data_in)[::-1], mask_type(mask)[::-1] + data_out = data_out[::-1] + + with self.subTest(data_in=data_in, mask=mask): + result = self.apply_mask(data_in, mask) + self.assertEqual(result, data_out) + + def test_apply_mask_check_input_types(self): + for data_in, mask in [(None, None), (b"abcd", None), (None, b"abcd")]: + with self.subTest(data_in=data_in, mask=mask): + with self.assertRaises(TypeError): + self.apply_mask(data_in, mask) + + def test_apply_mask_check_mask_length(self): + for data_in, mask in [ + (b"", b""), + (b"abcd", b"123"), + (b"", b"aBcDe"), + (b"12345678", b"12345678"), + ]: + with self.subTest(data_in=data_in, mask=mask): + with self.assertRaises(ValueError): + self.apply_mask(data_in, mask) + + +try: + from websockets.speedups import apply_mask as c_apply_mask +except ImportError: + pass +else: + + class SpeedupsTests(ApplyMaskTests): + @staticmethod + def apply_mask(*args, **kwargs): + try: + return c_apply_mask(*args, **kwargs) + except NotImplementedError as exc: # pragma: no cover + # PyPy doesn't implement creating contiguous readonly buffer + # from non-contiguous. We don't care about this edge case. + if ( + platform.python_implementation() == "PyPy" + and "not implemented yet" in str(exc) + ): + raise unittest.SkipTest(str(exc)) + else: + raise diff --git a/tests/wpt/tests/tools/third_party/websockets/tests/utils.py b/tests/wpt/tests/tools/third_party/websockets/tests/utils.py new file mode 100644 index 00000000000..2937a2f15e4 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tests/utils.py @@ -0,0 +1,88 @@ +import contextlib +import email.utils +import os +import pathlib +import platform +import tempfile +import time +import unittest +import warnings + + +# Generate TLS certificate with: +# $ openssl req -x509 -config test_localhost.cnf -days 15340 -newkey rsa:2048 \ +# -out test_localhost.crt -keyout test_localhost.key +# $ cat test_localhost.key test_localhost.crt > test_localhost.pem +# $ rm test_localhost.key test_localhost.crt + +CERTIFICATE = bytes(pathlib.Path(__file__).with_name("test_localhost.pem")) + + +DATE = email.utils.formatdate(usegmt=True) + + +# Unit for timeouts. May be increased on slow machines by setting the +# WEBSOCKETS_TESTS_TIMEOUT_FACTOR environment variable. +MS = 0.001 * float(os.environ.get("WEBSOCKETS_TESTS_TIMEOUT_FACTOR", "1")) + +# PyPy has a performance penalty for this test suite. +if platform.python_implementation() == "PyPy": # pragma: no cover + MS *= 5 + +# asyncio's debug mode has a 10x performance penalty for this test suite. +if os.environ.get("PYTHONASYNCIODEBUG"): # pragma: no cover + MS *= 10 + +# Ensure that timeouts are larger than the clock's resolution (for Windows). +MS = max(MS, 2.5 * time.get_clock_info("monotonic").resolution) + + +class GeneratorTestCase(unittest.TestCase): + """ + Base class for testing generator-based coroutines. + + """ + + def assertGeneratorRunning(self, gen): + """ + Check that a generator-based coroutine hasn't completed yet. + + """ + next(gen) + + def assertGeneratorReturns(self, gen): + """ + Check that a generator-based coroutine completes and return its value. + + """ + with self.assertRaises(StopIteration) as raised: + next(gen) + return raised.exception.value + + +class DeprecationTestCase(unittest.TestCase): + """ + Base class for testing deprecations. + + """ + + @contextlib.contextmanager + def assertDeprecationWarning(self, message): + """ + Check that a deprecation warning was raised with the given message. + + """ + with warnings.catch_warnings(record=True) as recorded_warnings: + warnings.simplefilter("always") + yield + + self.assertEqual(len(recorded_warnings), 1) + warning = recorded_warnings[0] + self.assertEqual(warning.category, DeprecationWarning) + self.assertEqual(str(warning.message), message) + + +@contextlib.contextmanager +def temp_unix_socket_path(): + with tempfile.TemporaryDirectory() as temp_dir: + yield str(pathlib.Path(temp_dir) / "websockets") diff --git a/tests/wpt/tests/tools/third_party/websockets/tox.ini b/tests/wpt/tests/tools/third_party/websockets/tox.ini new file mode 100644 index 00000000000..939d8c0cd80 --- /dev/null +++ b/tests/wpt/tests/tools/third_party/websockets/tox.ini @@ -0,0 +1,39 @@ +[tox] +envlist = + py37 + py38 + py39 + py310 + py311 + coverage + black + ruff + mypy + +[testenv] +commands = python -W error::DeprecationWarning -W error::PendingDeprecationWarning -m unittest {posargs} + +[testenv:coverage] +commands = + python -m coverage erase + python -m coverage run --source {envsitepackagesdir}/websockets,tests -m unittest {posargs} + python -m coverage report --show-missing --fail-under=100 +deps = coverage + +[testenv:maxi_cov] +commands = + python tests/maxi_cov.py {envsitepackagesdir} + python -m coverage report --show-missing --fail-under=100 +deps = coverage + +[testenv:black] +commands = black --check src tests +deps = black + +[testenv:ruff] +commands = ruff src tests +deps = ruff + +[testenv:mypy] +commands = mypy --strict src +deps = mypy diff --git a/tests/wpt/tests/tools/wave/requirements.txt b/tests/wpt/tests/tools/wave/requirements.txt index b2c151b8fe9..3bb476fd968 100644 --- a/tests/wpt/tests/tools/wave/requirements.txt +++ b/tests/wpt/tests/tools/wave/requirements.txt @@ -1,2 +1,2 @@ ua-parser==0.18.0 -python-dateutil==2.8.2 +python-dateutil==2.9.0.post0 diff --git a/tests/wpt/tests/tools/webtransport/h3/capsule.py b/tests/wpt/tests/tools/webtransport/h3/capsule.py index 74ca71ade9c..fc8183a65f0 100644 --- a/tests/wpt/tests/tools/webtransport/h3/capsule.py +++ b/tests/wpt/tests/tools/webtransport/h3/capsule.py @@ -108,7 +108,7 @@ class H3CapsuleDecoder: if self._final: raise e if not self._buffer: - return 0 + return size = self._buffer.capacity - self._buffer.tell() if size >= UINT_VAR_MAX_SIZE: raise e diff --git a/tests/wpt/tests/tools/wpt/requirements_android.txt b/tests/wpt/tests/tools/wpt/requirements_android.txt index 17672383cb8..e8205caa701 100644 --- a/tests/wpt/tests/tools/wpt/requirements_android.txt +++ b/tests/wpt/tests/tools/wpt/requirements_android.txt @@ -1 +1 @@ -mozrunner==8.3.0 +mozrunner==8.3.1 diff --git a/tests/wpt/tests/tools/wpt/requirements_install.txt b/tests/wpt/tests/tools/wpt/requirements_install.txt index 55bed99f8c5..f7df06548c9 100644 --- a/tests/wpt/tests/tools/wpt/requirements_install.txt +++ b/tests/wpt/tests/tools/wpt/requirements_install.txt @@ -1,2 +1,2 @@ mozinstall==2.1.0 -packaging==23.1 +packaging==24.0 diff --git a/tests/wpt/tests/tools/wpt/run.py b/tests/wpt/tests/tools/wpt/run.py index 9d3c98c5215..2277bb24439 100644 --- a/tests/wpt/tests/tools/wpt/run.py +++ b/tests/wpt/tests/tools/wpt/run.py @@ -565,6 +565,8 @@ class ChromeAndroidBase(BrowserSetup): if kwargs["package_name"] is None: kwargs["package_name"] = self.browser.find_binary( channel=browser_channel) + if not kwargs["device_serial"]: + kwargs["device_serial"] = ["emulator-5554"] if kwargs["webdriver_binary"] is None: webdriver_binary = None if not kwargs["install_webdriver"]: diff --git a/tests/wpt/tests/tools/wptrunner/requirements.txt b/tests/wpt/tests/tools/wptrunner/requirements.txt index 93c17bf3bff..356223ededc 100644 --- a/tests/wpt/tests/tools/wptrunner/requirements.txt +++ b/tests/wpt/tests/tools/wptrunner/requirements.txt @@ -3,7 +3,7 @@ mozdebug==0.3.0 mozinfo==1.2.3 # https://bugzilla.mozilla.org/show_bug.cgi?id=1621226 mozlog==8.0.0 mozprocess==1.3.1 -packaging==23.1 +packaging==24.0 pillow==9.5.0 requests==2.31.0 six==1.16.0 diff --git a/tests/wpt/tests/tools/wptrunner/requirements_firefox.txt b/tests/wpt/tests/tools/wptrunner/requirements_firefox.txt index 3ba4731494a..0ed5c053166 100644 --- a/tests/wpt/tests/tools/wptrunner/requirements_firefox.txt +++ b/tests/wpt/tests/tools/wptrunner/requirements_firefox.txt @@ -3,8 +3,8 @@ mozcrash==2.2.0 mozdevice==4.1.1 mozinstall==2.1.0 mozleak==0.2 -mozprofile==2.6.1 -mozrunner==8.3.0 +mozprofile==3.0.0 +mozrunner==8.3.1 mozversion==2.4.0 psutil==5.9.5 redo==2.0.4 diff --git a/tests/wpt/tests/tools/wptrunner/requirements_opera.txt b/tests/wpt/tests/tools/wptrunner/requirements_opera.txt index db0c5dd9923..6c2425f337b 100644 --- a/tests/wpt/tests/tools/wptrunner/requirements_opera.txt +++ b/tests/wpt/tests/tools/wptrunner/requirements_opera.txt @@ -1,2 +1,2 @@ mozprocess==1.3.1 -selenium==4.18.1 +selenium==4.20.0 diff --git a/tests/wpt/tests/tools/wptrunner/requirements_sauce.txt b/tests/wpt/tests/tools/wptrunner/requirements_sauce.txt index c9e42346ce4..806352e87e2 100644 --- a/tests/wpt/tests/tools/wptrunner/requirements_sauce.txt +++ b/tests/wpt/tests/tools/wptrunner/requirements_sauce.txt @@ -1,2 +1,2 @@ -selenium==4.18.1 +selenium==4.20.0 requests==2.31.0 diff --git a/tests/wpt/tests/tools/wptrunner/wptrunner/browsers/chrome.py b/tests/wpt/tests/tools/wptrunner/wptrunner/browsers/chrome.py index dfac89cb5ec..95a51451a32 100644 --- a/tests/wpt/tests/tools/wptrunner/wptrunner/browsers/chrome.py +++ b/tests/wpt/tests/tools/wptrunner/wptrunner/browsers/chrome.py @@ -138,8 +138,14 @@ def executor_kwargs(logger, test_type, test_environment, run_info_data, chrome_options["args"].append( "--ip-address-space-overrides=" + address_space_overrides_arg) + # Always disable antialiasing on the Ahem font. + blink_features = ['DisableAhemAntialias'] + if kwargs["enable_mojojs"]: - chrome_options["args"].append("--enable-blink-features=MojoJS,MojoJSTest") + blink_features.append('MojoJS') + blink_features.append('MojoJSTest') + + chrome_options["args"].append("--enable-blink-features=" + ','.join(blink_features)) if kwargs["enable_swiftshader"]: # https://chromium.googlesource.com/chromium/src/+/HEAD/docs/gpu/swiftshader.md diff --git a/tests/wpt/tests/tools/wptrunner/wptrunner/browsers/firefox.py b/tests/wpt/tests/tools/wptrunner/wptrunner/browsers/firefox.py index 814b8b8d758..d977930a289 100644 --- a/tests/wpt/tests/tools/wptrunner/wptrunner/browsers/firefox.py +++ b/tests/wpt/tests/tools/wptrunner/wptrunner/browsers/firefox.py @@ -132,6 +132,7 @@ def browser_kwargs(logger, test_type, run_info_data, config, subsuite, **kwargs) "headless": kwargs["headless"], "preload_browser": kwargs["preload_browser"] and not kwargs["pause_after_test"] and not kwargs["num_test_groups"] == 1, "specialpowers_path": kwargs["specialpowers_path"], + "allow_list_paths": kwargs["allow_list_paths"], "debug_test": kwargs["debug_test"]} if test_type == "wdspec" and kwargs["binary"]: browser_kwargs["webdriver_args"].extend(["--binary", kwargs["binary"]]) @@ -644,7 +645,8 @@ class GeckodriverOutputHandler(FirefoxOutputHandler): class ProfileCreator: def __init__(self, logger, prefs_root, config, test_type, extra_prefs, disable_fission, debug_test, browser_channel, binary, - package_name, certutil_binary, ca_certificate_path): + package_name, certutil_binary, ca_certificate_path, + allow_list_paths): self.logger = logger self.prefs_root = prefs_root self.config = config @@ -658,6 +660,7 @@ class ProfileCreator: self.package_name = package_name self.certutil_binary = certutil_binary self.ca_certificate_path = ca_certificate_path + self.allow_list_paths = allow_list_paths def create(self, **kwargs): """Create a Firefox profile and return the mozprofile Profile object pointing at that @@ -669,6 +672,7 @@ class ProfileCreator: profile = FirefoxProfile(preferences=preferences, restore=False, + allowlistpaths=self.allow_list_paths, **kwargs) self._set_required_prefs(profile) if self.ca_certificate_path is not None: @@ -795,7 +799,7 @@ class FirefoxBrowser(Browser): stackfix_dir=None, binary_args=None, timeout_multiplier=None, leak_check=False, asan=False, chaos_mode_flags=None, config=None, browser_channel="nightly", headless=None, preload_browser=False, - specialpowers_path=None, debug_test=False, **kwargs): + specialpowers_path=None, debug_test=False, allow_list_paths=None, **kwargs): Browser.__init__(self, logger) self.logger = logger @@ -826,7 +830,8 @@ class FirefoxBrowser(Browser): binary, package_name, certutil_binary, - ca_certificate_path) + ca_certificate_path, + allow_list_paths) if preload_browser: instance_manager_cls = PreloadInstanceManager @@ -899,7 +904,7 @@ class FirefoxWdSpecBrowser(WebDriverBrowser): disable_fission=False, stackfix_dir=None, leak_check=False, asan=False, chaos_mode_flags=None, config=None, browser_channel="nightly", headless=None, debug_test=False, profile_creator_cls=ProfileCreator, - **kwargs): + allow_list_paths=None, **kwargs): super().__init__(logger, binary, webdriver_binary, webdriver_args) self.binary = binary @@ -927,7 +932,8 @@ class FirefoxWdSpecBrowser(WebDriverBrowser): binary, package_name, certutil_binary, - ca_certificate_path) + ca_certificate_path, + allow_list_paths) self.profile = profile_creator.create() self.marionette_port = None diff --git a/tests/wpt/tests/tools/wptrunner/wptrunner/browsers/firefox_android.py b/tests/wpt/tests/tools/wptrunner/wptrunner/browsers/firefox_android.py index 7c158902e1a..526f83d595f 100644 --- a/tests/wpt/tests/tools/wptrunner/wptrunner/browsers/firefox_android.py +++ b/tests/wpt/tests/tools/wptrunner/wptrunner/browsers/firefox_android.py @@ -148,11 +148,13 @@ def get_environ(chaos_mode_flags, env_extras=None): class ProfileCreator(FirefoxProfileCreator): def __init__(self, logger, prefs_root, config, test_type, extra_prefs, disable_fission, debug_test, browser_channel, binary, - package_name, certutil_binary, ca_certificate_path): + package_name, certutil_binary, ca_certificate_path, + allow_list_paths=None): super().__init__(logger, prefs_root, config, test_type, extra_prefs, disable_fission, debug_test, browser_channel, None, - package_name, certutil_binary, ca_certificate_path) + package_name, certutil_binary, ca_certificate_path, + allow_list_paths) def _set_required_prefs(self, profile): profile.set_preferences({ diff --git a/tests/wpt/tests/tools/wptrunner/wptrunner/executors/base.py b/tests/wpt/tests/tools/wptrunner/wptrunner/executors/base.py index 763b6fcb19f..92a782e835c 100644 --- a/tests/wpt/tests/tools/wptrunner/wptrunner/executors/base.py +++ b/tests/wpt/tests/tools/wptrunner/wptrunner/executors/base.py @@ -313,7 +313,7 @@ class TestExecutor: result = self.do_test(test) except Exception as e: exception_string = traceback.format_exc() - message = f"Exception in TextExecutor.run:\n{exception_string}" + message = f"Exception in TestExecutor.run:\n{exception_string}" self.logger.warning(message) result = self.result_from_exception(test, e, exception_string) diff --git a/tests/wpt/tests/tools/wptrunner/wptrunner/executors/executorwebdriver.py b/tests/wpt/tests/tools/wptrunner/wptrunner/executors/executorwebdriver.py index 3a2f29bdc9d..69013e5e796 100644 --- a/tests/wpt/tests/tools/wptrunner/wptrunner/executors/executorwebdriver.py +++ b/tests/wpt/tests/tools/wptrunner/wptrunner/executors/executorwebdriver.py @@ -539,7 +539,9 @@ class WebDriverRun(TimedRunner): self.result = True, self.func(self.protocol, self.url, self.timeout) except (error.TimeoutException, error.ScriptTimeoutException): self.result = False, ("EXTERNAL-TIMEOUT", None) - except (socket.timeout, error.UnknownErrorException): + except socket.timeout: + # Checking if the browser is alive below is likely to hang, so mark + # this case as a CRASH unconditionally. self.result = False, ("CRASH", None) except Exception as e: if (isinstance(e, error.WebDriverException) and @@ -548,11 +550,12 @@ class WebDriverRun(TimedRunner): # workaround for https://bugs.chromium.org/p/chromedriver/issues/detail?id=2001 self.result = False, ("EXTERNAL-TIMEOUT", None) else: + status = "INTERNAL-ERROR" if self.protocol.is_alive() else "CRASH" message = str(getattr(e, "message", "")) if message: message += "\n" message += traceback.format_exc() - self.result = False, ("INTERNAL-ERROR", message) + self.result = False, (status, message) finally: self.result_flag.set() diff --git a/tests/wpt/tests/tools/wptrunner/wptrunner/wptrunner.py b/tests/wpt/tests/tools/wptrunner/wptrunner/wptrunner.py index d65369b380d..d9d85de6a4d 100644 --- a/tests/wpt/tests/tools/wptrunner/wptrunner/wptrunner.py +++ b/tests/wpt/tests/tools/wptrunner/wptrunner/wptrunner.py @@ -396,11 +396,16 @@ def run_tests(config, product, test_paths, **kwargs): product.check_args(**kwargs) + kwargs["allow_list_paths"] = [] if kwargs["install_fonts"]: + # Add test font to allow list for sandbox to ensure that the content + # processes will have read access. + ahem_path = os.path.join(test_paths["/"].tests_path, "fonts/Ahem.ttf") + kwargs["allow_list_paths"].append(ahem_path) env_extras.append(FontInstaller( logger, font_dir=kwargs["font_dir"], - ahem=os.path.join(test_paths["/"].tests_path, "fonts/Ahem.ttf") + ahem=ahem_path )) recording.set(["startup", "load_tests"]) diff --git a/tests/wpt/tests/trusted-types/TrustedTypePolicyFactory-getPropertyType.html b/tests/wpt/tests/trusted-types/TrustedTypePolicyFactory-getPropertyType.tentative.html index a1039487a24..e7218e9333a 100644 --- a/tests/wpt/tests/trusted-types/TrustedTypePolicyFactory-getPropertyType.html +++ b/tests/wpt/tests/trusted-types/TrustedTypePolicyFactory-getPropertyType.tentative.html @@ -19,11 +19,6 @@ assert_equals(trustedTypes.getAttributeType("img", "madeup"), null); }, "sanity check trustedTypes.getAttributeType."); - test(t => { - assert_true(!!trustedTypes.getTypeMapping()); - }, "sanity check trustedTypes.getTypeMapping"); - - // getPropertyType tests adapted from w3c/trusted-types polyfill: test(t => { // returns the proper type for attribute-related properties @@ -71,24 +66,6 @@ assert_equals(trustedTypes.getAttributeType('img', 'bar'), null); }, "getAttributeType tests adapted from w3c/trusted-types polyfill"); - - test(t=> { - const map = trustedTypes.getTypeMapping(); - - // Spot testing some values. - assert_equals(map["script"].attributes.src, "TrustedScriptURL"); - assert_equals(map["*"].properties.innerHTML, "TrustedHTML"); - assert_equals(map["foo"], undefined); - - // getTypeMapping returns a 'clean' object, in case the return value has - // been modified. - map["*"].attributes["foo"] = "bar"; - assert_equals(trustedTypes.getTypeMapping()["*"].attributes["foo"], undefined); -; - // Unknown namespaces: - assert_equals(trustedTypes.getTypeMapping("http://foo/bar"), null); - }, "getTypeMapping tests adapted from w3c/trusted-types polyfill"); - // Test case handling for both attributes and properties. for (let attr of ["srcDoc", "SRCDOC", "srcdoc"]) { for (let elem of ["iframe", "IFRAME", "iFrAmE"]) { diff --git a/tests/wpt/tests/trusted-types/TrustedTypePolicyFactory-metadata.html b/tests/wpt/tests/trusted-types/TrustedTypePolicyFactory-metadata.tentative.html index 70a5b446661..e7772bf0d15 100644 --- a/tests/wpt/tests/trusted-types/TrustedTypePolicyFactory-metadata.html +++ b/tests/wpt/tests/trusted-types/TrustedTypePolicyFactory-metadata.tentative.html @@ -33,22 +33,6 @@ let properties = ['madeup', 'id', "onerror", "onclick"]; const types = [null, "TrustedHTML", "TrustedScript", "TrustedScriptURL"]; - // We'll wrap construction of the elements/properties list in a test, mainly - // so we'll get decent error messages when it might fail. - test(t => { - // Collect all element and property names from getTypeMapping(). - const map = trustedTypes.getTypeMapping(); - for (let elem in map) { - elements.push(elem); - properties = properties.concat(Object.keys(map[elem].properties)); - } - - // Remove "*" entries and de-duplicate properties. - elements = elements.filter(s => !s.includes("*")); - properties = properties.filter(s => !s.includes("*")); - properties = Array.from(new Set(properties)); - }, "Prepare parameters for generic test series below."); - // Iterate one test for each combination of element, property, and sink type. const target = document.getElementById("target"); for (elem of elements) { diff --git a/tests/wpt/tests/webcodecs/video-encoder-flush.https.any.js b/tests/wpt/tests/webcodecs/video-encoder-flush.https.any.js index 8f1724bc857..c1ebafc4a3c 100644 --- a/tests/wpt/tests/webcodecs/video-encoder-flush.https.any.js +++ b/tests/wpt/tests/webcodecs/video-encoder-flush.https.any.js @@ -2,16 +2,36 @@ // META: script=/common/media.js // META: script=/webcodecs/utils.js // META: script=/webcodecs/video-encoder-utils.js +// META: variant=?vp8 +// META: variant=?h264_avc + +const VP8_CONFIG = { + codec: 'vp8', + width: 640, + height: 480, + displayWidth: 800, + displayHeight: 600, +}; + +const H264_AVC_CONFIG = { + codec: 'avc1.42001e', // Baseline + width: 640, + height: 480, + displayWidth: 800, + displayHeight: 600, + avc: {format: 'avc'}, +}; + +let CONFIG = null; +promise_setup(async () => { + CONFIG = { + '?vp8': VP8_CONFIG, + '?h264_avc': H264_AVC_CONFIG, + }[location.search]; +}); promise_test(async t => { let codecInit = getDefaultCodecInit(t); - let encoderConfig = { - codec: 'vp8', - width: 640, - height: 480, - displayWidth: 800, - displayHeight: 600, - }; let outputs = 0; let firstOutput = new Promise(resolve => { @@ -24,7 +44,7 @@ promise_test(async t => { }); let encoder = new VideoEncoder(codecInit); - encoder.configure(encoderConfig); + encoder.configure(CONFIG); let frame1 = createFrame(640, 480, 0); let frame2 = createFrame(640, 480, 33333); @@ -45,3 +65,48 @@ promise_test(async t => { assert_equals(outputs, 1, 'outputs'); }, 'Test reset during flush'); + +promise_test(async t => { + let frame1 = createFrame(640, 480, 0); + let frame2 = createFrame(640, 480, 33333); + t.add_cleanup(() => { + frame1.close(); + frame2.close(); + }); + + const callbacks = {}; + const encoder = createVideoEncoder(t, callbacks); + + let flushInCallbackDone; + let outputs = 0; + let firstOutput = new Promise(resolve => { + callbacks.output = (chunk, metadata) => { + encoder.reset(); + + callbacks.output = (chunk, metadata) => { + outputs++; + }; + + encoder.configure(CONFIG); + encoder.encode(frame2); + flushInCallbackDone = encoder.flush(); + + resolve(); + }; + }); + + encoder.configure(CONFIG); + encoder.encode(frame1); + const flushDone = encoder.flush(); + + // Wait for the first output, then reset. + await firstOutput; + + // Flush should have been synchronously rejected. + await promise_rejects_dom(t, 'AbortError', flushDone); + + // Wait for the second flush and check the output count. + await flushInCallbackDone; + + assert_equals(outputs, 1, 'outputs'); +}, 'Test new flush after reset in a flush callback'); diff --git a/tests/wpt/tests/webcodecs/video-encoder-utils.js b/tests/wpt/tests/webcodecs/video-encoder-utils.js index 0838260d31b..916f995156e 100644 --- a/tests/wpt/tests/webcodecs/video-encoder-utils.js +++ b/tests/wpt/tests/webcodecs/video-encoder-utils.js @@ -101,3 +101,22 @@ function createDottedFrame(width, height, dots, ts) { putBlackDots(ctx, width, height, dots); return new VideoFrame(cnv, { timestamp: ts, duration }); } + +function createVideoEncoder(t, callbacks) { + return new VideoEncoder({ + output(chunk, metadata) { + if (callbacks && callbacks.output) { + t.step(() => callbacks.output(chunk, metadata)); + } else { + t.unreached_func('unexpected output()'); + } + }, + error(e) { + if (callbacks && callbacks.error) { + t.step(() => callbacks.error(e)); + } else { + t.unreached_func('unexpected error()'); + } + } + }); +} diff --git a/tests/wpt/tests/webcodecs/videoDecoder-codec-specific.https.any.js b/tests/wpt/tests/webcodecs/videoDecoder-codec-specific.https.any.js index a3acb82ab26..1c3b8f120db 100644 --- a/tests/wpt/tests/webcodecs/videoDecoder-codec-specific.https.any.js +++ b/tests/wpt/tests/webcodecs/videoDecoder-codec-specific.https.any.js @@ -556,6 +556,75 @@ promise_test(async t => { const callbacks = {}; const decoder = createVideoDecoder(t, callbacks); + decoder.configure(CONFIG); + decoder.decode(CHUNKS[0]); + const flushDone = decoder.flush(); + + let flushDoneInCallback; + let outputs = 0; + await new Promise(resolve => { + callbacks.output = frame => { + decoder.reset(); + frame.close(); + + callbacks.output = frame => { + outputs++; + frame.close(); + }; + callbacks.error = e => { + t.unreached_func('unexpected error()'); + }; + decoder.configure(CONFIG); + decoder.decode(CHUNKS[0]); + flushDoneInCallback = decoder.flush(); + + resolve(); + }; + }); + + // First flush should have been synchronously rejected. + await promise_rejects_dom(t, 'AbortError', flushDone); + // Wait for the second flush and check the output count. + await flushDoneInCallback; + assert_equals(outputs, 1, 'outputs'); +}, 'Test new flush after reset in a flush callback'); + +promise_test(async t => { + await checkImplements(); + const callbacks = {}; + const decoder = createVideoDecoder(t, callbacks); + + decoder.configure(CONFIG); + decoder.decode(CHUNKS[0]); + const flushDone = decoder.flush(); + let flushDoneInCallback; + + await new Promise(resolve => { + callbacks.output = frame => { + decoder.reset(); + frame.close(); + + callbacks.output = frame => { frame.close(); }; + decoder.configure(CONFIG); + decoder.decode(CHUNKS[0]); + decoder.decode(createCorruptChunk(1)); + flushDoneInCallback = decoder.flush(); + + resolve(); + }; + }); + + // First flush should have been synchronously rejected. + await promise_rejects_dom(t, 'AbortError', flushDone); + // Wait for the second flush and check the error in the rejected promise. + await promise_rejects_dom(t, 'EncodingError', flushDoneInCallback); +}, 'Test decoding a corrupt frame after reset in a flush callback'); + +promise_test(async t => { + await checkImplements(); + const callbacks = {}; + const decoder = createVideoDecoder(t, callbacks); + decoder.configure({...CONFIG, optimizeForLatency: true}); decoder.decode(CHUNKS[0]); diff --git a/tests/wpt/tests/webcodecs/videoFrame-copyTo-rgb.any.js b/tests/wpt/tests/webcodecs/videoFrame-copyTo-rgb.any.js new file mode 100644 index 00000000000..442efc4b0f4 --- /dev/null +++ b/tests/wpt/tests/webcodecs/videoFrame-copyTo-rgb.any.js @@ -0,0 +1,252 @@ +// META: global=window,dedicatedworker +// META: script=/webcodecs/videoFrame-utils.js +// META: script=/webcodecs/video-encoder-utils.js + +function compareColors(actual, expected, tolerance, msg) { + let channel = ['R', 'G', 'B', 'A']; + for (let i = 0; i < 4; i++) { + assert_approx_equals( + actual[i], expected[i], tolerance, + `${msg} ${channel[i]}: actual: ${actual[i]} expected: ${expected[i]}`); + } +} + +function rgb2yuv(r, g, b) { + let y = r * .299000 + g * .587000 + b * .114000 + let u = r * -.168736 + g * -.331264 + b * .500000 + 128 + let v = r * .500000 + g * -.418688 + b * -.081312 + 128 + + y = Math.round(y); + u = Math.round(u); + v = Math.round(v); + return { + y, u, v + } +} + +function makeI420Frames() { + const kYellow = {r: 0xFF, g: 0xFF, b: 0x00}; + const kRed = {r: 0xFF, g: 0x00, b: 0x00}; + const kBlue = {r: 0x00, g: 0x00, b: 0xFF}; + const kGreen = {r: 0x00, g: 0xFF, b: 0x00}; + const kPink = {r: 0xFF, g: 0x78, b: 0xFF}; + const kMagenta = {r: 0xFF, g: 0x00, b: 0xFF}; + const kBlack = {r: 0x00, g: 0x00, b: 0x00}; + const kWhite = {r: 0xFF, g: 0xFF, b: 0xFF}; + const smpte170m = { + matrix: 'smpte170m', + primaries: 'smpte170m', + transfer: 'smpte170m', + fullRange: false + }; + const bt709 = { + matrix: 'bt709', + primaries: 'bt709', + transfer: 'bt709', + fullRange: false + }; + + const result = []; + const init = {format: 'I420', timestamp: 0, codedWidth: 4, codedHeight: 4}; + const colors = + [kYellow, kRed, kBlue, kGreen, kMagenta, kBlack, kWhite, kPink]; + const data = new Uint8Array(24); + for (let colorSpace of [null, smpte170m, bt709]) { + init.colorSpace = colorSpace; + result.push(new VideoFrame(data, init)); + for (let color of colors) { + color = rgb2yuv(color.r, color.g, color.b); + data.fill(color.y, 0, 16); + data.fill(color.u, 16, 20); + data.fill(color.v, 20, 24); + result.push(new VideoFrame(data, init)); + } + } + return result; +} + +function makeRGBXFrames() { + const kYellow = 0xFFFF00; + const kRed = 0xFF0000; + const kBlue = 0x0000FF; + const kGreen = 0x00FF00; + const kBlack = 0x000000; + const kWhite = 0xFFFFFF; + const smpte170m = { + matrix: 'smpte170m', + primaries: 'smpte170m', + transfer: 'smpte170m', + fullRange: false + }; + const bt709 = { + matrix: 'bt709', + primaries: 'bt709', + transfer: 'bt709', + fullRange: false + }; + + const result = []; + const init = {format: 'RGBX', timestamp: 0, codedWidth: 4, codedHeight: 4}; + const colors = [kYellow, kRed, kBlue, kGreen, kBlack, kWhite]; + const data = new Uint32Array(16); + for (let colorSpace of [null, smpte170m, bt709]) { + init.colorSpace = colorSpace; + for (let color of colors) { + data.fill(color, 0, 16); + result.push(new VideoFrame(data, init)); + } + } + return result; +} + +async function testFrame(frame, colorSpace, pixelFormat) { + const width = frame.visibleRect.width; + const height = frame.visibleRect.height; + let frame_message = 'Frame: ' + JSON.stringify({ + format: frame.format, + width: width, + height: height, + matrix: frame.colorSpace?.matrix, + primaries: frame.colorSpace?.primaries, + transfer: frame.colorSpace?.transfer, + }); + const cnv = new OffscreenCanvas(width, height); + const ctx = + cnv.getContext('2d', {colorSpace: colorSpace, willReadFrequently: true}); + + // Read VideoFrame pixels via copyTo() + let imageData = ctx.createImageData(width, height); + let copy_to_buf = imageData.data.buffer; + let layout = null; + try { + const options = { + rect: {x: 0, y: 0, width: width, height: height}, + format: pixelFormat, + colorSpace: colorSpace + }; + layout = await frame.copyTo(copy_to_buf, options); + } catch (e) { + assert_unreached(`copyTo() failure: ${e}`); + return; + } + if (layout.length != 1) { + assert_unreached('Conversion to RGB is not supported by the browser'); + return; + } + + // Read VideoFrame pixels via drawImage() + ctx.drawImage(frame, 0, 0, width, height, 0, 0, width, height); + imageData = ctx.getImageData(0, 0, width, height, {colorSpace: colorSpace}); + let get_image_buf = imageData.data.buffer; + + // Compare! + const tolerance = 1; + for (let i = 0; i < copy_to_buf.byteLength; i += 4) { + if (pixelFormat.startsWith('BGR')) { + // getImageData() always gives us RGB, we need to swap bytes before + // comparing them with BGR. + new Uint8Array(get_image_buf, i, 3).reverse(); + } + compareColors( + new Uint8Array(copy_to_buf, i, 4), new Uint8Array(get_image_buf, i, 4), + tolerance, frame_message + ` Mismatch at offset ${i}`); + } +} + +function test_4x4_I420_frames() { + for (let colorSpace of ['srgb', 'display-p3']) { + for (let pixelFormat of ['RGBA', 'RGBX', 'BGRA', 'BGRX']) { + promise_test(async t => { + for (let frame of makeI420Frames()) { + await testFrame(frame, colorSpace, pixelFormat); + frame.close(); + } + }, `Convert 4x4 I420 frames to ${pixelFormat} / ${colorSpace}`); + } + } +} +test_4x4_I420_frames(); + +function test_4x4_RGB_frames() { + for (let colorSpace of ['srgb', 'display-p3']) { + for (let pixelFormat of ['RGBA', 'RGBX', 'BGRA', 'BGRX']) { + promise_test(async t => { + for (let frame of makeRGBXFrames()) { + await testFrame(frame, colorSpace, pixelFormat); + frame.close(); + } + }, `Convert 4x4 RGBX frames to ${pixelFormat} / ${colorSpace}`); + } + } +} +test_4x4_RGB_frames(); + + +function test_4color_canvas_frames() { + for (let colorSpace of ['srgb', 'display-p3']) { + for (let pixelFormat of ['RGBA', 'RGBX', 'BGRA', 'BGRX']) { + promise_test(async t => { + const frame = createFrame(32, 16); + await testFrame(frame, colorSpace, pixelFormat); + frame.close(); + }, `Convert 4-color canvas frame to ${pixelFormat} / ${colorSpace}`); + } + } +} +test_4color_canvas_frames(); + +promise_test(async t => { + let pixelFormat = 'RGBA' + const init = {format: 'RGBA', timestamp: 0, codedWidth: 4, codedHeight: 4}; + const src_data = new Uint32Array(init.codedWidth * init.codedHeight); + src_data.fill(0xFFFFFFFF); + const offset = 5; + const stride = 40; + const dst_data = new Uint8Array(offset + stride * init.codedHeight); + const options = { + format: pixelFormat, + layout: [ + {offset: offset, stride: stride}, + ] + }; + const frame = new VideoFrame(src_data, init); + await frame.copyTo(dst_data, options) + assert_false(dst_data.slice(0, offset).some(e => e != 0), 'offset'); + for (let row = 0; row < init.codedHeight; ++row) { + let width = init.codedWidth * 4; + const row_data = + dst_data.slice(offset + stride * row, offset + stride * row + width); + const margin_data = dst_data.slice( + offset + stride * row + width, offset + stride * (row + 1)); + + assert_false( + row_data.some(e => e != 0xFF), + `unexpected data in row ${row} [${row_data}]`); + assert_false( + margin_data.some(e => e != 0), + `unexpected margin in row ${row} [${margin_data}]`); + } + + frame.close(); +}, `copyTo() with layout`); + +function test_unsupported_pixel_formats() { + const kUnsupportedFormats = [ + 'I420', 'I420P10', 'I420P12', 'I420A', 'I422', 'I422A', 'I444', 'I444A', + 'NV12' + ]; + + for (let pixelFormat of kUnsupportedFormats) { + promise_test(async t => { + const init = + {format: 'RGBX', timestamp: 0, codedWidth: 4, codedHeight: 4}; + const data = new Uint32Array(16); + const options = {format: pixelFormat}; + const frame = new VideoFrame(data, init); + await promise_rejects_dom( + t, 'NotSupportedError', frame.copyTo(data, options)) + frame.close(); + }, `Unsupported format ${pixelFormat}`); + } +} +test_unsupported_pixel_formats(); diff --git a/tests/wpt/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/capture_screenshot.py b/tests/wpt/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/capture_screenshot.py index 40497ce6ac7..414f5ae2d01 100644 --- a/tests/wpt/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/capture_screenshot.py +++ b/tests/wpt/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/capture_screenshot.py @@ -1,6 +1,6 @@ import pytest -from math import floor +from math import ceil, floor from tests.support.image import png_dimensions from . import get_physical_viewport_dimensions @@ -79,3 +79,29 @@ async def test_capture_with_viewport(bidi_session, new_tab, delta_width, delta_h result = await bidi_session.browsing_context.capture_screenshot( context=new_tab["context"]) assert png_dimensions(result) == (expected_size["width"], expected_size["height"]) + + +@pytest.mark.parametrize("dpr", [0.5, 2]) +@pytest.mark.asyncio +async def test_capture_with_different_dpr(bidi_session, new_tab, inline, dpr): + page = inline("<div style='background-color: black; width: 100px; height: 100px;'></div>") + await bidi_session.browsing_context.navigate( + context=new_tab["context"], url=page, wait="complete" + ) + + original_viewport = await get_viewport_dimensions(bidi_session, new_tab) + + await bidi_session.browsing_context.set_viewport( + context=new_tab["context"], + device_pixel_ratio=dpr) + + expected_width = original_viewport["width"] * dpr + expected_height = original_viewport["height"] * dpr + + data = await bidi_session.browsing_context.capture_screenshot(context=new_tab["context"]) + (actual_width, actual_height) = png_dimensions(data) + # The rounding is implementation-specific and can be either floor, ceil or round depending on the browser + # implementation. Tolerate any value between floor and ceil. + assert floor(expected_width) <= actual_width <= ceil(expected_width) + assert floor(expected_height) <= actual_height <= ceil(expected_height) + diff --git a/tests/wpt/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/clip.py b/tests/wpt/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/clip.py index 8300e962b92..67d4b0d06c4 100644 --- a/tests/wpt/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/clip.py +++ b/tests/wpt/tests/webdriver/tests/bidi/browsing_context/capture_screenshot/clip.py @@ -373,3 +373,42 @@ async def test_clip_element_outside_of_window_viewport( comparison = await compare_png_bidi(reference_data, data) assert comparison.equal() + + +@pytest.mark.parametrize("dpr", [0.5, 2]) +async def test_clip_with_different_dpr(bidi_session, new_tab, inline, compare_png_bidi, dpr): + div_size = {"width": 100, "height": 100} + + reference_page = inline(f"""<div style='background-color: black; width: {div_size["width"]*dpr}px; height: {div_size["height"]*dpr}px;'></div>""") + await bidi_session.browsing_context.navigate( + context=new_tab["context"], url=reference_page, wait="complete" + ) + element = await bidi_session.script.evaluate( + await_promise=False, + expression="document.querySelector('div')", + target=ContextTarget(new_tab["context"]), + ) + reference_data = await bidi_session.browsing_context.capture_screenshot( + context=new_tab["context"], clip=ElementOptions(element=element) + ) + + page = inline(f"""<div style='background-color: black; width: {div_size["width"]}px; height: {div_size["height"]}px;'></div>""") + await bidi_session.browsing_context.navigate( + context=new_tab["context"], url=page, wait="complete" + ) + + await bidi_session.browsing_context.set_viewport( + context=new_tab["context"], + device_pixel_ratio=dpr) + + element = await bidi_session.script.evaluate( + await_promise=False, + expression="document.querySelector('div')", + target=ContextTarget(new_tab["context"]), + ) + data = await bidi_session.browsing_context.capture_screenshot( + context=new_tab["context"], clip=ElementOptions(element=element) + ) + + comparison = await compare_png_bidi(data, reference_data) + assert comparison.equal() diff --git a/tests/wpt/tests/webdriver/tests/bidi/browsing_context/set_viewport/device_pixel_ratio.py b/tests/wpt/tests/webdriver/tests/bidi/browsing_context/set_viewport/device_pixel_ratio.py index e4db779bd5d..76a4ef7ecd2 100644 --- a/tests/wpt/tests/webdriver/tests/bidi/browsing_context/set_viewport/device_pixel_ratio.py +++ b/tests/wpt/tests/webdriver/tests/bidi/browsing_context/set_viewport/device_pixel_ratio.py @@ -1,4 +1,5 @@ import pytest +from webdriver.bidi.modules.script import ContextTarget from ... import get_device_pixel_ratio, get_viewport_dimensions @@ -68,3 +69,47 @@ async def test_reset_device_pixel_ratio(bidi_session, inline, new_tab): device_pixel_ratio=None) assert await get_device_pixel_ratio(bidi_session, new_tab) == original_dpr + + +@pytest.mark.asyncio +@pytest.mark.parametrize("device_pixel_ratio", [0.5, 2]) +@pytest.mark.parametrize( + "use_horizontal_scrollbar, use_vertical_scrollbar", + [ + (True, False), + (False, True), + (True, True), + ], + ids=["horizontal", "vertical", "both"], +) +async def test_device_pixel_ratio_with_scrollbar( + bidi_session, + inline, + new_tab, + device_pixel_ratio, + use_horizontal_scrollbar, + use_vertical_scrollbar, +): + viewport_dimensions = await get_viewport_dimensions(bidi_session, new_tab) + + width = 100 + if use_horizontal_scrollbar: + width = viewport_dimensions["width"] + 100 + + height = 100 + if use_vertical_scrollbar: + height = viewport_dimensions["height"] + 100 + + html = f"""<div style="width: {width}px; height: {height}px;">foo</div>""" + page_url = inline(html) + + await bidi_session.browsing_context.navigate( + context=new_tab["context"], url=page_url, wait="complete" + ) + + await bidi_session.browsing_context.set_viewport( + context=new_tab["context"], device_pixel_ratio=device_pixel_ratio + ) + + assert await get_device_pixel_ratio(bidi_session, new_tab) == device_pixel_ratio + assert await get_viewport_dimensions(bidi_session, new_tab) == viewport_dimensions diff --git a/tests/wpt/tests/webdriver/tests/bidi/network/response_completed/response_completed.py b/tests/wpt/tests/webdriver/tests/bidi/network/response_completed/response_completed.py index 56b94616425..f17a522766e 100644 --- a/tests/wpt/tests/webdriver/tests/bidi/network/response_completed/response_completed.py +++ b/tests/wpt/tests/webdriver/tests/bidi/network/response_completed/response_completed.py @@ -159,7 +159,7 @@ async def test_load_page_twice( @pytest.mark.parametrize( "status, status_text", - [(status, text) for (status, text) in HTTP_STATUS_AND_STATUS_TEXT if status not in [101, 407]], + HTTP_STATUS_AND_STATUS_TEXT, ) @pytest.mark.asyncio async def test_response_status( diff --git a/tests/wpt/tests/webdriver/tests/bidi/network/response_completed/response_completed_status.py b/tests/wpt/tests/webdriver/tests/bidi/network/response_completed/response_completed_status.py deleted file mode 100644 index 36e3da667e9..00000000000 --- a/tests/wpt/tests/webdriver/tests/bidi/network/response_completed/response_completed_status.py +++ /dev/null @@ -1,55 +0,0 @@ -# TODO(#42482): Merge this file with response_completed.py -# -# The status codes in this file are currently problematic in some implementations. -# -# The only mechanism currently provided by WPT to disable subtests with -# expectations is to disable the entire file. As such, this file is a copy of -# response_completed.py with the problematic status codes extracted. -# -# Once it is possible to disable subtests, this file should be merged with -# response_completed.py. - -import pytest - -from .. import ( - assert_response_event, - HTTP_STATUS_AND_STATUS_TEXT, - RESPONSE_COMPLETED_EVENT, -) - - -@pytest.mark.parametrize( - "status, status_text", - [(status, text) for (status, text) in HTTP_STATUS_AND_STATUS_TEXT if status in [101, 407]], -) -@pytest.mark.asyncio -async def test_response_status( - wait_for_event, wait_for_future_safe, url, fetch, setup_network_test, status, status_text -): - status_url = url( - f"/webdriver/tests/support/http_handlers/status.py?status={status}&nocache={RESPONSE_COMPLETED_EVENT}" - ) - - network_events = await setup_network_test(events=[RESPONSE_COMPLETED_EVENT]) - events = network_events[RESPONSE_COMPLETED_EVENT] - - on_response_completed = wait_for_event(RESPONSE_COMPLETED_EVENT) - await fetch(status_url) - await wait_for_future_safe(on_response_completed) - - assert len(events) == 1 - expected_request = {"method": "GET", "url": status_url} - expected_response = { - "url": status_url, - "fromCache": False, - "mimeType": "text/plain", - "status": status, - "statusText": status_text, - "protocol": "http/1.1", - } - assert_response_event( - events[0], - expected_request=expected_request, - expected_response=expected_response, - redirect_count=0, - ) diff --git a/tests/wpt/tests/webdriver/tests/bidi/network/response_started/response_started.py b/tests/wpt/tests/webdriver/tests/bidi/network/response_started/response_started.py index 6c10714ca81..9aa77739abc 100644 --- a/tests/wpt/tests/webdriver/tests/bidi/network/response_started/response_started.py +++ b/tests/wpt/tests/webdriver/tests/bidi/network/response_started/response_started.py @@ -135,6 +135,32 @@ async def test_load_page_twice( ) +@pytest.mark.asyncio +async def test_request_bodysize( + wait_for_event, wait_for_future_safe, url, fetch, setup_network_test +): + html_url = url(PAGE_EMPTY_HTML) + + network_events = await setup_network_test(events=[RESPONSE_STARTED_EVENT]) + events = network_events[RESPONSE_STARTED_EVENT] + + on_before_request_sent = wait_for_event(RESPONSE_STARTED_EVENT) + await fetch(html_url, method="POST", post_data="{'a': 1}") + await wait_for_future_safe(on_before_request_sent) + + assert len(events) == 1 + expected_request = { + "method": "POST", + "url": html_url, + } + assert_response_event( + events[0], + expected_request=expected_request, + redirect_count=0, + ) + assert events[0]["request"]["bodySize"] > 0 + + @pytest.mark.parametrize( "status, status_text", HTTP_STATUS_AND_STATUS_TEXT, diff --git a/tests/wpt/tests/webdriver/tests/support/fixtures_bidi.py b/tests/wpt/tests/webdriver/tests/support/fixtures_bidi.py index 32919210bf2..c76b369f21c 100644 --- a/tests/wpt/tests/webdriver/tests/support/fixtures_bidi.py +++ b/tests/wpt/tests/webdriver/tests/support/fixtures_bidi.py @@ -538,7 +538,12 @@ def fetch(bidi_session, top_context, configuration): """ async def fetch( - url, method="GET", headers=None, context=top_context, timeout_in_seconds=3 + url, + method="GET", + headers=None, + post_data=None, + context=top_context, + timeout_in_seconds=3, ): method_arg = f"method: '{method}'," @@ -546,6 +551,10 @@ def fetch(bidi_session, top_context, configuration): if headers is not None: headers_arg = f"headers: {json.dumps(headers)}," + body_arg = "" + if post_data is not None: + body_arg = f"body: {post_data}," + timeout_in_seconds = timeout_in_seconds * configuration["timeout_multiplier"] # Wait for fetch() to resolve a response and for response.text() to # resolve as well to make sure the request/response is completed when @@ -558,6 +567,7 @@ def fetch(bidi_session, top_context, configuration): fetch("{url}", {{ {method_arg} {headers_arg} + {body_arg} signal: controller.signal, }}).then(response => response.text()); }}""", diff --git a/tests/wpt/tests/webidl/ecmascript-binding/legacy-factory-function-builtin-properties.window.js b/tests/wpt/tests/webidl/ecmascript-binding/legacy-factory-function-builtin-properties.window.js new file mode 100644 index 00000000000..fc5c48aca38 --- /dev/null +++ b/tests/wpt/tests/webidl/ecmascript-binding/legacy-factory-function-builtin-properties.window.js @@ -0,0 +1,6 @@ +"use strict"; + +test(() => { + const ownPropKeys = Reflect.ownKeys(Image).slice(0, 3); + assert_array_equals(ownPropKeys, ["length", "name", "prototype"]); +}, 'Legacy factory function property enumeration order of "length", "name", and "prototype"'); diff --git a/tests/wpt/tests/webnn/conformance_tests/gpu/parallel-compute.https.any.js b/tests/wpt/tests/webnn/conformance_tests/gpu/parallel-compute.https.any.js new file mode 100644 index 00000000000..13f2757b54e --- /dev/null +++ b/tests/wpt/tests/webnn/conformance_tests/gpu/parallel-compute.https.any.js @@ -0,0 +1,10 @@ +// META: title=test parallel WebNN API compute operations +// META: global=window,dedicatedworker +// META: script=../../resources/utils.js +// META: timeout=long + +'use strict'; + +// https://webmachinelearning.github.io/webnn/#api-mlcontext-compute + +testParallelCompute('gpu'); diff --git a/tests/wpt/tests/webnn/conformance_tests/parallel-compute.https.any.js b/tests/wpt/tests/webnn/conformance_tests/parallel-compute.https.any.js new file mode 100644 index 00000000000..304ea5f202f --- /dev/null +++ b/tests/wpt/tests/webnn/conformance_tests/parallel-compute.https.any.js @@ -0,0 +1,10 @@ +// META: title=test parallel WebNN API compute operations +// META: global=window,dedicatedworker +// META: script=../resources/utils.js +// META: timeout=long + +'use strict'; + +// https://webmachinelearning.github.io/webnn/#api-mlcontext-compute + +testParallelCompute(); diff --git a/tests/wpt/tests/webnn/resources/test_data/prelu.json b/tests/wpt/tests/webnn/resources/test_data/prelu.json index cf79bee7a9d..14a7c412dd7 100644 --- a/tests/wpt/tests/webnn/resources/test_data/prelu.json +++ b/tests/wpt/tests/webnn/resources/test_data/prelu.json @@ -1,6 +1,34 @@ { "tests": [ { + "name": "prelu float32 0D scalar", + "inputs": { + "x": { + "shape": [], + "data": [ + -4.794857500523286 + ], + "type": "float32" + }, + "slope": { + "shape": [], + "data": [ + 1.1202747481570352 + ], + "type": "float32", + "constant": true + } + }, + "expected": { + "name": "output", + "shape": [], + "data": [ + -5.371557712554932 + ], + "type": "float32" + } + }, + { "name": "prelu float32 1D constant tensors", "inputs": { "x": { diff --git a/tests/wpt/tests/webnn/resources/test_data/softplus.json b/tests/wpt/tests/webnn/resources/test_data/softplus.json index eb05b7b2811..373612d5ca2 100644 --- a/tests/wpt/tests/webnn/resources/test_data/softplus.json +++ b/tests/wpt/tests/webnn/resources/test_data/softplus.json @@ -1,7 +1,7 @@ -{ // softplus: The calculation follows the expression ln(1 + exp(steepness * x)) / steepness. +{ // softplus: The calculation follows the expression ln(1 + exp(x)). "tests": [ { - "name": "softplus float32 1D constant tensor default options", // default options: {steepness: 1} + "name": "softplus float32 1D constant tensor", "inputs": { "x": { "shape": [24], @@ -68,7 +68,7 @@ } }, { - "name": "softplus float32 1D tensor default options", // default options: {steepness: 1} + "name": "softplus float32 1D tensor", "inputs": { "x": { "shape": [24], @@ -134,7 +134,7 @@ } }, { - "name": "softplus float32 2D tensor default options", + "name": "softplus float32 2D tensor", "inputs": { "x": { "shape": [4, 6], @@ -200,7 +200,7 @@ } }, { - "name": "softplus float32 3D tensor default options", + "name": "softplus float32 3D tensor", "inputs": { "x": { "shape": [2, 3, 4], @@ -266,7 +266,7 @@ } }, { - "name": "softplus float32 4D tensor default options", + "name": "softplus float32 4D tensor", "inputs": { "x": { "shape": [1, 2, 3, 4], @@ -332,7 +332,7 @@ } }, { - "name": "softplus float32 5D tensor default options", + "name": "softplus float32 5D tensor", "inputs": { "x": { "shape": [1, 2, 1, 3, 4], @@ -396,144 +396,6 @@ ], "type": "float32" } - }, - { - "name": "softplus both positive float32 4D tensor and options.steepness", - "inputs": { - "x": { - "shape": [1, 2, 3, 4], - "data": [ - 5.626614582460632, - 5.167487045486892, - 4.0146356193402655, - 9.48003299650489, - 9.989938045769978, - 7.0654412821434125, - 2.132681001794825, - 8.187151346059956, - 5.169976220175496, - 2.1044997879382077, - 3.523329401138895, - 4.136340646976668, - 1.7418719794295656, - 5.145224066290767, - 5.015515309165462, - 0.045903935074711466, - 2.9570898924917377, - 3.959244712098706, - 5.517926978255181, - 7.192322388417094, - 8.76492480390928, - 1.3734704039113388, - 8.930669016709397, - 8.660283210871246 - ], - "type": "float32" - } - }, - "options": { - "steepness": 1.5104469060897827 - }, - "expected": { - "name": "output", - "shape": [1, 2, 3, 4], - "data": [ - 5.626749515533447, - 5.167757034301758, - 4.016173362731934, - 9.480032920837402, - 9.989937782287598, - 7.065456390380859, - 2.158585548400879, - 8.187153816223145, - 5.170245170593262, - 2.1315081119537354, - 3.526555061340332, - 4.137620449066162, - 1.7879058122634888, - 5.145503044128418, - 5.015854835510254, - 0.4822517931461334, - 2.964651584625244, - 3.960916519165039, - 5.518085956573486, - 7.19233512878418, - 8.764925956726074, - 1.4518096446990967, - 8.930669784545898, - 8.660284042358398 - ], - "type": "float32" - } - }, - { - "name": "softplus both negative float32 4D tensor and options.steepness", - "inputs": { - "x": { - "shape": [1, 2, 3, 4], - "data": [ - -5.584833476104802, - -8.188738740810354, - -8.981280004134987, - -1.7315531899284586, - -0.7266543578958906, - -0.0034800119290885334, - -7.378389455552106, - -8.907525953796949, - -6.0483786568116304, - -6.328561142365743, - -2.6006513567654626, - -5.02005264196455, - -2.0647716093484414, - -1.5499896740695434, - -2.221591675966657, - -1.1088025713211636, - -2.7854626064634385, - -2.105037489961294, - -5.144277741727352, - -5.081219916574497, - -7.499426297617635, - -2.4305558382286545, - -8.390520024268328, - -0.07117499202643174 - ], - "type": "float32" - } - }, - "options": { - "steepness": -1.2985155767552126 - }, - "expected": { - "name": "output", - "shape": [1, 2, 3, 4], - "data": [ - -5.585379123687744, - -8.188756942749023, - -8.981287002563477, - -1.8088372945785522, - -0.9798305630683899, - -0.5355416536331177, - -7.378442287445068, - -8.907533645629883, - -6.048677444458008, - -6.328769207000732, - -2.626511573791504, - -5.021188259124756, - -2.1157851219177246, - -1.6465802192687988, - -2.2634570598602295, - -1.2725814580917358, - -2.805877923965454, - -2.1535322666168213, - -5.145244121551514, - -5.082269191741943, - -7.499471664428711, - -2.4626762866973877, - -8.390534400939941, - -0.5702091455459595 - ], - "type": "float32" - } } ] }
\ No newline at end of file diff --git a/tests/wpt/tests/webnn/resources/utils.js b/tests/wpt/tests/webnn/resources/utils.js index d1dc0675a7f..653e036cd11 100644 --- a/tests/wpt/tests/webnn/resources/utils.js +++ b/tests/wpt/tests/webnn/resources/utils.js @@ -901,6 +901,40 @@ const testWebNNOperation = (operationName, buildFunc, deviceType = 'cpu') => { }); }; +/** + * WebNN parallel compute operation test. + * @param {String} deviceType - The execution device type for this test. + */ +const testParallelCompute = (deviceType = 'cpu') => { + let ml_context; + let ml_graph; + + promise_setup(async () => { + ml_context = await navigator.ml.createContext({deviceType}); + // Construct a simple graph: A = B * 2. + const builder = new MLGraphBuilder(ml_context); + const operandType = {dataType: 'float32', dimensions: [1]}; + const input_operand = builder.input('input', operandType); + const const_operand = builder.constant(operandType, Float32Array.from([2])); + const output_operand = builder.mul(input_operand, const_operand); + ml_graph = await builder.build({'output': output_operand}); + }); + + promise_test(async () => { + const test_inputs = [1, 2, 3, 4]; + + const actual_outputs = await Promise.all(test_inputs.map(async (input) => { + let inputs = {'input': Float32Array.from([input])}; + let outputs = {'output': new Float32Array(1)}; + ({inputs, outputs} = await ml_context.compute(ml_graph, inputs, outputs)); + return outputs.output[0]; + })); + + const expected_outputs = [2, 4, 6, 8]; + assert_array_equals(actual_outputs, expected_outputs); + }); +}; + // ref: http://stackoverflow.com/questions/32633585/how-do-you-convert-to-half-floats-in-javascript const toHalf = (value) => { let floatView = new Float32Array(1); diff --git a/tests/wpt/tests/webnn/validation_tests/clamp.https.any.js b/tests/wpt/tests/webnn/validation_tests/clamp.https.any.js index 85cd19a566a..126fa90e167 100644 --- a/tests/wpt/tests/webnn/validation_tests/clamp.https.any.js +++ b/tests/wpt/tests/webnn/validation_tests/clamp.https.any.js @@ -5,3 +5,56 @@ 'use strict'; validateInputFromAnotherBuilder('clamp'); + +validateUnaryOperation( + 'clamp', allWebNNOperandDataTypes, /*alsoBuildActivation=*/ true); + +promise_test(async t => { + const options = {minValue: 1.0, maxValue: 3.0}; + const input = + builder.input('input', {dataType: 'uint32', dimensions: [1, 2, 3]}); + const output = builder.clamp(input, options); + assert_equals(output.dataType(), 'uint32'); + assert_array_equals(output.shape(), [1, 2, 3]); +}, '[clamp] Test building an operator with options'); + +promise_test(async t => { + const options = {minValue: 0, maxValue: 0}; + const input = + builder.input('input', {dataType: 'int32', dimensions: [1, 2, 3, 4]}); + const output = builder.clamp(input, options); + assert_equals(output.dataType(), 'int32'); + assert_array_equals(output.shape(), [1, 2, 3, 4]); +}, '[clamp] Test building an operator with options.minValue == options.maxValue'); + +promise_test(async t => { + const options = {minValue: 2.0}; + builder.clamp(options); +}, '[clamp] Test building an activation with options'); + +promise_test(async t => { + const options = {minValue: 3.0, maxValue: 1.0}; + const input = + builder.input('input', {dataType: 'uint8', dimensions: [1, 2, 3]}); + assert_throws_js(TypeError, () => builder.clamp(input, options)); +}, '[clamp] Throw if options.minValue > options.maxValue when building an operator'); + +// To be removed once infinite `minValue` is allowed. Tracked in +// https://github.com/webmachinelearning/webnn/pull/647. +promise_test(async t => { + const options = {minValue: -Infinity}; + const input = builder.input('input', {dataType: 'float16', dimensions: []}); + assert_throws_js(TypeError, () => builder.clamp(input, options)); +}, '[clamp] Throw if options.minValue is -Infinity when building an operator'); + +promise_test(async t => { + const options = {minValue: 2.0, maxValue: -1.0}; + assert_throws_js(TypeError, () => builder.clamp(options)); +}, '[clamp] Throw if options.minValue > options.maxValue when building an activation'); + +// To be removed once NaN `maxValue` is allowed. Tracked in +// https://github.com/webmachinelearning/webnn/pull/647. +promise_test(async t => { + const options = {maxValue: NaN}; + assert_throws_js(TypeError, () => builder.clamp(options)); +}, '[clamp] Throw if options.maxValue is NaN when building an activation'); diff --git a/tests/wpt/tests/webnn/validation_tests/conv2d.https.any.js b/tests/wpt/tests/webnn/validation_tests/conv2d.https.any.js index ffc9c2c65df..7dac654951b 100644 --- a/tests/wpt/tests/webnn/validation_tests/conv2d.https.any.js +++ b/tests/wpt/tests/webnn/validation_tests/conv2d.https.any.js @@ -55,3 +55,478 @@ multi_builder_test(async (t, builder, otherBuilder) => { const filter = builder.input('filter', kExampleFilterDescriptor); assert_throws_js(TypeError, () => builder.conv2d(input, filter, options)); }, '[conv2d] throw if activation option is from another builder'); + +const tests = [ + { + name: '[conv2d] Test with default options.', + input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 1, 3, 3]}, + output: {dataType: 'float32', dimensions: [1, 1, 3, 3]} + }, + { + name: '[conv2d] Test with padding.', + input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 1, 3, 3]}, + options: { + padding: [1, 1, 1, 1], + }, + output: {dataType: 'float32', dimensions: [1, 1, 5, 5]} + }, + { + name: '[conv2d] Test with strides and padding.', + input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 1, 3, 3]}, + options: { + padding: [1, 1, 1, 1], + strides: [2, 2], + }, + output: {dataType: 'float32', dimensions: [1, 1, 3, 3]} + }, + { + name: '[conv2d] Test with strides and asymmetric padding.', + input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 1, 4, 2]}, + options: { + padding: [1, 2, 0, 1], + strides: [2, 2], + }, + output: {dataType: 'float32', dimensions: [1, 1, 3, 3]} + }, + { + name: '[conv2d] Test depthwise conv2d by setting groups to input channels.', + input: {dataType: 'float32', dimensions: [1, 4, 2, 2]}, + filter: {dataType: 'float32', dimensions: [4, 1, 2, 2]}, + options: { + groups: 4, + }, + output: {dataType: 'float32', dimensions: [1, 4, 1, 1]} + }, + { + name: + '[conv2d] Test depthwise conv2d with groups, inputLayout="nhwc" and filterLayout="ihwo".', + input: {dataType: 'float32', dimensions: [1, 2, 2, 4]}, + filter: {dataType: 'float32', dimensions: [1, 2, 2, 4]}, + options: { + groups: 4, + inputLayout: 'nhwc', + filterLayout: 'ihwo', + }, + output: {dataType: 'float32', dimensions: [1, 1, 1, 4]} + }, + { + name: + '[conv2d] Test with dilations, inputLayout="nhwc" and filterLayout="ihwo".', + input: {dataType: 'float32', dimensions: [1, 65, 65, 1]}, + filter: {dataType: 'float32', dimensions: [1, 3, 3, 1]}, + options: { + inputLayout: 'nhwc', + filterLayout: 'ihwo', + dilations: [4, 4], + }, + output: {dataType: 'float32', dimensions: [1, 57, 57, 1]} + }, + { + name: '[conv2d] Test with inputLayout="nchw" and filterLayout="oihw".', + input: {dataType: 'float32', dimensions: [1, 2, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 2, 3, 3]}, + options: { + inputLayout: 'nchw', + filterLayout: 'oihw', + }, + output: {dataType: 'float32', dimensions: [1, 1, 3, 3]} + }, + { + name: '[conv2d] Test with inputLayout="nchw" and filterLayout="hwio".', + input: {dataType: 'float32', dimensions: [1, 2, 5, 5]}, + filter: {dataType: 'float32', dimensions: [3, 3, 2, 1]}, + options: { + inputLayout: 'nchw', + filterLayout: 'hwio', + }, + output: {dataType: 'float32', dimensions: [1, 1, 3, 3]} + }, + { + name: '[conv2d] Test with inputLayout="nchw" and filterLayout="ohwi".', + input: {dataType: 'float32', dimensions: [1, 2, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 3, 3, 2]}, + options: { + inputLayout: 'nchw', + filterLayout: 'ohwi', + }, + output: {dataType: 'float32', dimensions: [1, 1, 3, 3]} + }, + { + name: '[conv2d] Test with inputLayout="nchw" and filterLayout="ihwo".', + input: {dataType: 'float32', dimensions: [1, 2, 5, 5]}, + filter: {dataType: 'float32', dimensions: [2, 3, 3, 1]}, + options: { + inputLayout: 'nchw', + filterLayout: 'ihwo', + }, + output: {dataType: 'float32', dimensions: [1, 1, 3, 3]} + }, + { + name: '[conv2d] Test with inputLayout="nhwc" and filterLayout="oihw".', + input: {dataType: 'float32', dimensions: [1, 5, 5, 2]}, + filter: {dataType: 'float32', dimensions: [1, 2, 3, 3]}, + options: { + inputLayout: 'nhwc', + filterLayout: 'oihw', + }, + output: {dataType: 'float32', dimensions: [1, 3, 3, 1]} + }, + { + name: '[conv2d] Test with inputLayout="nhwc" and filterLayout="hwio".', + input: {dataType: 'float32', dimensions: [1, 5, 5, 2]}, + filter: {dataType: 'float32', dimensions: [3, 3, 2, 1]}, + options: { + inputLayout: 'nhwc', + filterLayout: 'hwio', + }, + output: {dataType: 'float32', dimensions: [1, 3, 3, 1]} + }, + { + name: '[conv2d] Test with inputLayout="nhwc" and filterLayout="ohwi".', + input: {dataType: 'float32', dimensions: [1, 5, 5, 2]}, + filter: {dataType: 'float32', dimensions: [1, 3, 3, 2]}, + options: { + inputLayout: 'nhwc', + filterLayout: 'ohwi', + }, + output: {dataType: 'float32', dimensions: [1, 3, 3, 1]} + }, + { + name: '[conv2d] Test with inputLayout="nhwc" and filterLayout="ihwo".', + input: {dataType: 'float32', dimensions: [1, 5, 5, 2]}, + filter: {dataType: 'float32', dimensions: [2, 3, 3, 1]}, + options: { + inputLayout: 'nhwc', + filterLayout: 'ihwo', + }, + output: {dataType: 'float32', dimensions: [1, 3, 3, 1]} + }, + { + name: '[conv2d] Throw if the input is not a 4-D tensor.', + input: {dataType: 'float32', dimensions: [1, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 2, 2, 1]}, + }, + { + name: '[conv2d] Throw if the input data type is not floating point.', + input: {dataType: 'int32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'int32', dimensions: [1, 1, 2, 2]}, + }, + { + name: '[conv2d] Throw if the filter is not a 4-D tensor.', + input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'float32', dimensions: [2, 2]}, + }, + { + name: + '[conv2d] Throw if the filter data type doesn\'t match the input data type.', + input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'int32', dimensions: [1, 1, 2, 2]}, + }, + { + name: '[conv2d] Throw if the length of padding is not 4.', + input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]}, + options: { + padding: [2, 2], + }, + }, + { + name: '[conv2d] Throw if the length of strides is not 2.', + input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]}, + options: { + strides: [2], + }, + }, + { + name: '[conv2d] Throw if strideHeight is smaller than 1.', + input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]}, + options: { + strides: [0, 1], + }, + }, + { + name: '[conv2d] Throw if strideWidth is smaller than 1.', + input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]}, + options: { + strides: [1, 0], + }, + }, + { + name: '[conv2d] Throw if the length of dilations is not 2.', + input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]}, + options: { + dilations: [1], + }, + }, + { + name: '[conv2d] Throw if dilationHeight is smaller than 1.', + input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]}, + options: { + dilations: [0, 1], + }, + }, + { + name: '[conv2d] Throw if dilationWidth is smaller than 1.', + input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]}, + options: { + dilations: [1, 0], + }, + }, + { + name: '[conv2d] Throw if inputChannels % groups is not 0.', + input: {dataType: 'float32', dimensions: [1, 4, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]}, + options: { + groups: 3, + }, + }, + { + name: + '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels.', + input: {dataType: 'float32', dimensions: [1, 4, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]}, + options: { + groups: 2, + }, + }, + { + name: '[conv2d] Throw if the groups is smaller than 1.', + input: {dataType: 'float32', dimensions: [1, 4, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]}, + options: { + groups: 0, + }, + }, + { + name: + '[conv2d] Throw due to overflow when calculating the effective filter height.', + input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 1, 434983, 2]}, + options: { + dilations: [328442, 1], + }, + }, + { + name: + '[conv2d] Throw due to overflow when calculating the effective filter width.', + input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 1, 2, 234545]}, + options: { + dilations: [2, 843452], + }, + }, + { + name: '[conv2d] Throw due to overflow when dilation height is too large.', + input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 1, 3, 3]}, + options: { + dilations: [kMaxUnsignedLong, 1], + }, + }, + { + name: '[conv2d] Throw due to overflow when dilation width is too large.', + input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 1, 3, 3]}, + options: { + dilations: [1, kMaxUnsignedLong], + }, + }, + { + name: '[conv2d] Throw due to underflow when calculating the output height.', + input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 1, 4, 2]}, + options: { + dilations: [4, 1], + padding: [1, 1, 1, 1], + strides: [2, 2], + }, + }, + { + name: '[conv2d] Throw due to underflow when calculating the output width.', + input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 1, 2, 8]}, + options: { + dilations: [1, 4], + padding: [1, 1, 1, 1], + strides: [2, 2], + }, + }, + { + name: '[conv2d] Throw if the bias is not a 1-D tensor.', + input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]}, + options: { + bias: {dataType: 'float32', dimensions: [1, 2]}, + }, + }, + { + name: + '[conv2d] Throw if the bias shape is not equal to [output_channels] with filterLayout="oihw".', + input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]}, + options: { + bias: {dataType: 'float32', dimensions: [2]}, + }, + }, + { + name: + '[conv2d] Throw if the bias shape is not equal to [output_channels] with filterLayout="hwio".', + input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'float32', dimensions: [2, 2, 1, 1]}, + options: { + bias: {dataType: 'float32', dimensions: [2]}, + }, + }, + { + name: + '[conv2d] Throw if the bias shape is not equal to [output_channels] with filterLayout="ohwi".', + input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 2, 2, 1]}, + options: { + bias: {dataType: 'float32', dimensions: [2]}, + }, + }, + { + name: + '[conv2d] Throw if the bias shape is not equal to [output_channels] with filterLayout="ihwo".', + input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 2, 2, 1]}, + options: { + bias: {dataType: 'float32', dimensions: [2]}, + }, + }, + { + name: + '[conv2d] Throw if the bias data type doesn\'t match input data type.', + input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]}, + options: { + bias: {dataType: 'int32', dimensions: [1]}, + }, + }, + { + name: + '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels with inputLayout="nchw" and filterLayout="oihw".', + input: {dataType: 'float32', dimensions: [1, 2, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 2, 3, 3]}, + options: { + inputLayout: 'nchw', + filterLayout: 'oihw', + groups: 2, + }, + }, + { + name: + '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels with inputLayout="nchw" and filterLayout="hwio".', + input: {dataType: 'float32', dimensions: [1, 2, 5, 5]}, + filter: {dataType: 'float32', dimensions: [3, 3, 2, 1]}, + options: { + inputLayout: 'nchw', + filterLayout: 'hwio', + groups: 2, + }, + }, + { + name: + '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels with inputLayout="nchw" and filterLayout="ohwi".', + input: {dataType: 'float32', dimensions: [1, 2, 5, 5]}, + filter: {dataType: 'float32', dimensions: [1, 3, 3, 2]}, + options: { + inputLayout: 'nchw', + filterLayout: 'ohwi', + groups: 2, + }, + }, + { + name: + '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels with inputLayout="nchw" and filterLayout="ihwo".', + input: {dataType: 'float32', dimensions: [1, 2, 5, 5]}, + filter: {dataType: 'float32', dimensions: [2, 3, 3, 1]}, + options: { + inputLayout: 'nchw', + filterLayout: 'ihwo', + groups: 2, + }, + + }, + { + name: + '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels with inputLayout="nhwc" and filterLayout="oihw".', + input: {dataType: 'float32', dimensions: [1, 5, 5, 2]}, + filter: {dataType: 'float32', dimensions: [1, 2, 3, 3]}, + options: { + inputLayout: 'nhwc', + filterLayout: 'oihw', + groups: 2, + }, + }, + { + name: + '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels with inputLayout="nhwc" and filterLayout="hwio".', + input: {dataType: 'float32', dimensions: [1, 5, 5, 2]}, + filter: {dataType: 'float32', dimensions: [3, 3, 2, 1]}, + options: { + inputLayout: 'nhwc', + filterLayout: 'hwio', + groups: 2, + }, + }, + { + name: + '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels with inputLayout="nhwc" and filterLayout="ohwi".', + input: {dataType: 'float32', dimensions: [1, 5, 5, 2]}, + filter: {dataType: 'float32', dimensions: [1, 3, 3, 2]}, + options: { + inputLayout: 'nhwc', + filterLayout: 'ohwi', + groups: 2, + }, + }, + { + name: + '[conv2d] Throw if inputChannels / groups is not equal to filterInputChannels with inputLayout="nhwc" and filterLayout="ihwo".', + input: {dataType: 'float32', dimensions: [1, 5, 5, 2]}, + filter: {dataType: 'float32', dimensions: [2, 3, 3, 1]}, + options: { + inputLayout: 'nhwc', + filterLayout: 'ihwo', + groups: 2, + }, + }, +]; + +tests.forEach( + test => promise_test(async t => { + const input = builder.input( + 'input', + {dataType: test.input.dataType, dimensions: test.input.dimensions}); + const filter = builder.input( + 'filter', + {dataType: test.filter.dataType, dimensions: test.filter.dimensions}); + + if (test.options && test.options.bias) { + test.options.bias = builder.input('bias', { + dataType: test.options.bias.dataType, + dimensions: test.options.bias.dimensions + }); + } + + if (test.output) { + const output = builder.conv2d(input, filter, test.options); + assert_equals(output.dataType(), test.output.dataType); + assert_array_equals(output.shape(), test.output.dimensions); + } else { + assert_throws_js( + TypeError, () => builder.conv2d(input, filter, test.options)); + } + }, test.name)); diff --git a/tests/wpt/tests/webnn/validation_tests/convTranspose2d.https.any.js b/tests/wpt/tests/webnn/validation_tests/convTranspose2d.https.any.js index 3c3c4956643..02822c52749 100644 --- a/tests/wpt/tests/webnn/validation_tests/convTranspose2d.https.any.js +++ b/tests/wpt/tests/webnn/validation_tests/convTranspose2d.https.any.js @@ -196,6 +196,12 @@ const tests = [ filter: {dataType: 'float32', dimensions: [1, 1, 2, 2]}, }, { + name: + '[convTranspose2d] Throw if the input data type is not floating point.', + input: {dataType: 'int32', dimensions: [1, 1, 5, 5]}, + filter: {dataType: 'int32', dimensions: [1, 1, 2, 2]}, + }, + { name: '[convTranspose2d] Throw if the filter is not a 4-D tensor.', input: {dataType: 'float32', dimensions: [1, 1, 5, 5]}, filter: {dataType: 'float32', dimensions: [2, 2]}, diff --git a/tests/wpt/tests/webnn/validation_tests/elu.https.any.js b/tests/wpt/tests/webnn/validation_tests/elu.https.any.js index 6e842cb691a..53ec5e54ae2 100644 --- a/tests/wpt/tests/webnn/validation_tests/elu.https.any.js +++ b/tests/wpt/tests/webnn/validation_tests/elu.https.any.js @@ -5,3 +5,43 @@ 'use strict'; validateInputFromAnotherBuilder('elu'); + +validateUnaryOperation( + 'elu', floatingPointTypes, /*alsoBuildActivation=*/ true); + +promise_test(async t => { + const options = {alpha: 1.0}; + const input = + builder.input('input', {dataType: 'float32', dimensions: [1, 2, 3]}); + const output = builder.elu(input, options); + assert_equals(output.dataType(), 'float32'); + assert_array_equals(output.shape(), [1, 2, 3]); +}, '[elu] Test building an operator with options'); + +promise_test(async t => { + const options = {alpha: 1.5}; + builder.elu(options); +}, '[elu] Test building an activation with options'); + +promise_test(async t => { + const options = {alpha: -1.0}; + const input = + builder.input('input', {dataType: 'float32', dimensions: [1, 2, 3]}); + assert_throws_js(TypeError, () => builder.elu(input, options)); +}, '[elu] Throw if options.alpha <= 0 when building an operator'); + +promise_test(async t => { + const options = {alpha: NaN}; + const input = builder.input('input', {dataType: 'float16', dimensions: []}); + assert_throws_js(TypeError, () => builder.elu(input, options)); +}, '[elu] Throw if options.alpha is NaN when building an operator'); + +promise_test(async t => { + const options = {alpha: 0}; + assert_throws_js(TypeError, () => builder.elu(options)); +}, '[elu] Throw if options.alpha <= 0 when building an activation'); + +promise_test(async t => { + const options = {alpha: Infinity}; + assert_throws_js(TypeError, () => builder.elu(options)); +}, '[elu] Throw if options.alpha is Infinity when building an activation'); diff --git a/tests/wpt/tests/webnn/validation_tests/expand.https.any.js b/tests/wpt/tests/webnn/validation_tests/expand.https.any.js index d90ab894689..088d826df7a 100644 --- a/tests/wpt/tests/webnn/validation_tests/expand.https.any.js +++ b/tests/wpt/tests/webnn/validation_tests/expand.https.any.js @@ -12,3 +12,66 @@ multi_builder_test(async (t, builder, otherBuilder) => { assert_throws_js( TypeError, () => builder.expand(inputFromOtherBuilder, newShape)); }, '[expand] throw if input is from another builder'); + +const tests = [ + { + name: '[expand] Test with 0-D scalar to 3-D tensor.', + input: {dataType: 'float32', dimensions: []}, + newShape: [3, 4, 5], + output: {dataType: 'float32', dimensions: [3, 4, 5]} + }, + { + name: '[expand] Test with the new shapes that are the same as input.', + input: {dataType: 'float32', dimensions: [4]}, + newShape: [4], + output: {dataType: 'float32', dimensions: [4]} + }, + { + name: '[expand] Test with the new shapes that are broadcastable.', + input: {dataType: 'int32', dimensions: [3, 1, 5]}, + newShape: [3, 4, 5], + output: {dataType: 'int32', dimensions: [3, 4, 5]} + }, + { + name: + '[expand] Test with the new shapes that are broadcastable and the rank of new shapes is larger than input.', + input: {dataType: 'int32', dimensions: [2, 5]}, + newShape: [3, 2, 5], + output: {dataType: 'int32', dimensions: [3, 2, 5]} + }, + { + name: + '[expand] Throw if the input shapes are the same rank but not broadcastable.', + input: {dataType: 'uint32', dimensions: [3, 6, 2]}, + newShape: [4, 3, 5], + }, + { + name: '[expand] Throw if the input shapes are not broadcastable.', + input: {dataType: 'uint32', dimensions: [5, 4]}, + newShape: [5], + }, + { + name: '[expand] Throw if the number of new shapes is too large.', + input: {dataType: 'float32', dimensions: [1, 2, 1, 1]}, + newShape: [1, 2, kMaxUnsignedLong, kMaxUnsignedLong], + }, +]; + +tests.forEach( + test => promise_test(async t => { + const input = builder.input( + 'input', + {dataType: test.input.dataType, dimensions: test.input.dimensions}); + const options = {}; + if (test.axis) { + options.axis = test.axis; + } + + if (test.output) { + const output = builder.expand(input, test.newShape); + assert_equals(output.dataType(), test.output.dataType); + assert_array_equals(output.shape(), test.output.dimensions); + } else { + assert_throws_js(TypeError, () => builder.expand(input, test.newShape)); + } + }, test.name)); diff --git a/tests/wpt/tests/webnn/validation_tests/gelu.https.any.js b/tests/wpt/tests/webnn/validation_tests/gelu.https.any.js new file mode 100644 index 00000000000..c758c61f4c3 --- /dev/null +++ b/tests/wpt/tests/webnn/validation_tests/gelu.https.any.js @@ -0,0 +1,10 @@ +// META: title=validation tests for WebNN API gelu operation +// META: global=window,dedicatedworker +// META: script=../resources/utils_validation.js + +'use strict'; + +validateInputFromAnotherBuilder('gelu'); + +validateUnaryOperation( + 'gelu', floatingPointTypes, /*alsoBuildActivation=*/ true); diff --git a/tests/wpt/tests/webnn/validation_tests/gemm.https.any.js b/tests/wpt/tests/webnn/validation_tests/gemm.https.any.js index 77ce6383ccf..abe0ba61936 100644 --- a/tests/wpt/tests/webnn/validation_tests/gemm.https.any.js +++ b/tests/wpt/tests/webnn/validation_tests/gemm.https.any.js @@ -19,3 +19,143 @@ multi_builder_test(async (t, builder, otherBuilder) => { const b = builder.input('b', kExampleInputDescriptor); assert_throws_js(TypeError, () => builder.gemm(a, b, options)); }, '[gemm] throw if c option is from another builder'); + +const tests = [ + { + name: '[gemm] Test building gemm with default option.', + a: {dataType: 'float32', dimensions: [2, 3]}, + b: {dataType: 'float32', dimensions: [3, 4]}, + output: {dataType: 'float32', dimensions: [2, 4]} + }, + { + name: + '[gemm] Throw if inputShapeA[1] is not equal to inputShapeB[0] default options.', + a: {dataType: 'float32', dimensions: [2, 3]}, + b: {dataType: 'float32', dimensions: [2, 4]}, + }, + { + name: '[gemm] Test building gemm with aTranspose=true.', + a: {dataType: 'float32', dimensions: [2, 3]}, + b: {dataType: 'float32', dimensions: [2, 4]}, + options: { + aTranspose: true, + }, + output: {dataType: 'float32', dimensions: [3, 4]} + }, + { + name: + '[gemm] Throw if inputShapeA[0] is not equal to inputShapeB[0] with aTranspose=true.', + a: {dataType: 'float32', dimensions: [2, 3]}, + b: {dataType: 'float32', dimensions: [3, 4]}, + options: { + aTranspose: true, + }, + }, + { + name: '[gemm] Test building gemm with bTranspose=true.', + a: {dataType: 'float32', dimensions: [2, 3]}, + b: {dataType: 'float32', dimensions: [4, 3]}, + options: { + bTranspose: true, + }, + output: {dataType: 'float32', dimensions: [2, 4]} + }, + { + name: + '[gemm] Throw if inputShapeA[0] is not equal to inputShapeB[0] with bTranspose=true.', + a: {dataType: 'float32', dimensions: [2, 3]}, + b: {dataType: 'float32', dimensions: [3, 4]}, + options: { + bTranspose: true, + }, + }, + { + name: '[gemm] Throw if the rank of inputA is not 2.', + a: {dataType: 'float32', dimensions: [2, 3, 1]}, + b: {dataType: 'float32', dimensions: [2, 4]}, + }, + { + name: '[gemm] Throw if the rank of inputB is not 2.', + a: {dataType: 'float32', dimensions: [2, 4]}, + b: {dataType: 'float32', dimensions: [2, 3, 1]}, + }, + { + name: '[gemm] Throw if data types of two inputs do not match.', + a: {dataType: 'float32', dimensions: [2, 3]}, + b: {dataType: 'float16', dimensions: [3, 4]}, + }, + { + name: '[gemm] Test building gemm with inputC.', + a: {dataType: 'float32', dimensions: [2, 3]}, + b: {dataType: 'float32', dimensions: [3, 4]}, + options: { + c: {dataType: 'float32', dimensions: [4]}, + }, + output: {dataType: 'float32', dimensions: [2, 4]} + }, + { + name: '[gemm] Test building gemm with scalar inputC.', + a: {dataType: 'float32', dimensions: [2, 3]}, + b: {dataType: 'float32', dimensions: [3, 4]}, + options: { + c: {dataType: 'float32', dimensions: []}, + }, + output: {dataType: 'float32', dimensions: [2, 4]} + }, + { + name: + '[gemm] Throw if inputShapeC is not unidirectionally broadcastable to the output shape [inputShapeA[0], inputShapeB[1]].', + a: {dataType: 'float32', dimensions: [2, 3]}, + b: {dataType: 'float32', dimensions: [3, 4]}, + options: { + c: {dataType: 'float32', dimensions: [2, 3]}, + }, + }, + { + name: '[gemm] Throw if the input data type is not floating point.', + a: {dataType: 'int32', dimensions: [2, 3]}, + b: {dataType: 'int32', dimensions: [3, 4]} + }, + { + name: + '[gemm] Throw if data type of inputC does not match ones of inputA and inputB.', + a: {dataType: 'float32', dimensions: [3, 2]}, + b: {dataType: 'float32', dimensions: [4, 3]}, + options: { + c: {dataType: 'float16', dimensions: [2, 4]}, + aTranspose: true, + bTranspose: true, + }, + }, + { + name: '[gemm] Throw if the rank of inputC is 3.', + a: {dataType: 'float32', dimensions: [3, 2]}, + b: {dataType: 'float32', dimensions: [4, 3]}, + options: { + c: {dataType: 'float32', dimensions: [2, 3, 4]}, + aTranspose: true, + bTranspose: true, + }, + }, +]; + +tests.forEach( + test => promise_test(async t => { + const a = builder.input( + 'a', {dataType: test.a.dataType, dimensions: test.a.dimensions}); + const b = builder.input( + 'b', {dataType: test.b.dataType, dimensions: test.b.dimensions}); + if (test.options && test.options.c) { + test.options.c = builder.input('c', { + dataType: test.options.c.dataType, + dimensions: test.options.c.dimensions + }); + } + if (test.output) { + const output = builder.gemm(a, b, test.options); + assert_equals(output.dataType(), test.output.dataType); + assert_array_equals(output.shape(), test.output.dimensions); + } else { + assert_throws_js(TypeError, () => builder.gemm(a, b, test.options)); + } + }, test.name)); diff --git a/tests/wpt/tests/webnn/validation_tests/hardSigmoid.https.any.js b/tests/wpt/tests/webnn/validation_tests/hardSigmoid.https.any.js index 01b24dbc7c6..2c55d0eb9df 100644 --- a/tests/wpt/tests/webnn/validation_tests/hardSigmoid.https.any.js +++ b/tests/wpt/tests/webnn/validation_tests/hardSigmoid.https.any.js @@ -5,3 +5,31 @@ 'use strict'; validateInputFromAnotherBuilder('hardSigmoid'); + +validateUnaryOperation( + 'hardSigmoid', floatingPointTypes, /*alsoBuildActivation=*/ true); + +promise_test(async t => { + const options = {alpha: 0.5, beta: 1.0}; + const input = + builder.input('input', {dataType: 'float16', dimensions: [1, 2, 3]}); + const output = builder.hardSigmoid(input, options); + assert_equals(output.dataType(), 'float16'); + assert_array_equals(output.shape(), [1, 2, 3]); +}, '[hardSigmoid] Test building an operator with options'); + +promise_test(async t => { + const options = {alpha: 0.2}; + builder.hardSigmoid(options); +}, '[hardSigmoid] Test building an activation with options'); + +promise_test(async t => { + const options = {beta: NaN}; + const input = builder.input('input', {dataType: 'float32', dimensions: []}); + assert_throws_js(TypeError, () => builder.hardSigmoid(input, options)); +}, '[hardSigmoid] Throw if options.beta is NaN when building an operator'); + +promise_test(async t => { + const options = {alpha: Infinity}; + assert_throws_js(TypeError, () => builder.hardSigmoid(options)); +}, '[hardSigmoid] Throw if options.alpha is Infinity when building an activation'); diff --git a/tests/wpt/tests/webnn/validation_tests/layerNormalization.https.any.js b/tests/wpt/tests/webnn/validation_tests/layerNormalization.https.any.js index e9e9141aa6c..63f9c0dbc58 100644 --- a/tests/wpt/tests/webnn/validation_tests/layerNormalization.https.any.js +++ b/tests/wpt/tests/webnn/validation_tests/layerNormalization.https.any.js @@ -9,8 +9,6 @@ const kExampleInputDescriptor = { dimensions: [2, 2] }; -validateOptionsAxes('layerNormalization', 4); - validateInputFromAnotherBuilder('layerNormalization'); multi_builder_test(async (t, builder, otherBuilder) => { @@ -30,3 +28,181 @@ multi_builder_test(async (t, builder, otherBuilder) => { const input = builder.input('input', kExampleInputDescriptor); assert_throws_js(TypeError, () => builder.layerNormalization(input, options)); }, '[layerNormalization] throw if bias option is from another builder'); + +const tests = [ + { + name: '[layerNormalization] Test with default options for scalar input.', + input: {dataType: 'float32', dimensions: []}, + output: {dataType: 'float32', dimensions: []}, + }, + { + name: '[layerNormalization] Test when the input data type is float16.', + input: {dataType: 'float16', dimensions: []}, + output: {dataType: 'float16', dimensions: []}, + }, + { + name: '[layerNormalization] Test with given axes.', + input: {dataType: 'float32', dimensions: [1, 2, 3, 4]}, + options: { + axes: [3], + }, + output: {dataType: 'float32', dimensions: [1, 2, 3, 4]}, + }, + { + name: '[layerNormalization] Test with given scale.', + input: {dataType: 'float32', dimensions: [1, 2, 3, 4]}, + options: { + scale: {dataType: 'float32', dimensions: [2, 3, 4]}, + }, + output: {dataType: 'float32', dimensions: [1, 2, 3, 4]}, + }, + { + name: '[layerNormalization] Test with a non-default epsilon value.', + input: {dataType: 'float32', dimensions: [1, 2, 3, 4]}, + options: { + epsilon: 1e-4, // default epsilon=1e-5 + }, + output: {dataType: 'float32', dimensions: [1, 2, 3, 4]}, + }, + { + name: '[layerNormalization] Test with given axes, scale and bias.', + input: {dataType: 'float32', dimensions: [1, 2, 3, 4]}, + options: { + scale: {dataType: 'float32', dimensions: [3, 4]}, + bias: {dataType: 'float32', dimensions: [3, 4]}, + axes: [2, 3], + }, + output: {dataType: 'float32', dimensions: [1, 2, 3, 4]}, + }, + { + name: '[layerNormalization] Test with nonconsecutive axes.', + input: {dataType: 'float32', dimensions: [1, 2, 3, 4, 5, 6]}, + options: { + scale: {dataType: 'float32', dimensions: [2, 4, 6]}, + bias: {dataType: 'float32', dimensions: [2, 4, 6]}, + axes: [1, 3, 5], + }, + output: {dataType: 'float32', dimensions: [1, 2, 3, 4, 5, 6]}, + }, + { + name: '[layerNormalization] Test with axes in descending order.', + input: {dataType: 'float32', dimensions: [1, 2, 3, 4, 5, 6]}, + options: { + scale: {dataType: 'float32', dimensions: [6, 5, 4, 3, 2]}, + bias: {dataType: 'float32', dimensions: [6, 5, 4, 3, 2]}, + axes: [5, 4, 3, 2, 1] + }, + output: {dataType: 'float32', dimensions: [1, 2, 3, 4, 5, 6]}, + }, + { + name: + '[layerNormalization] Throw if the input data type is not one of the floating point types.', + input: {dataType: 'uint32', dimensions: [1, 2, 3, 4]}, + }, + { + name: + '[layerNormalization] Throw if the axis is greater than the input rank.', + input: {dataType: 'float32', dimensions: [1, 2, 3, 4]}, + options: { + axes: [1, 2, 4], + }, + }, + { + name: '[layerNormalization] Throw if the axes have duplications.', + input: {dataType: 'float32', dimensions: [1, 2, 3, 4]}, + options: {axes: [3, 3]}, + }, + { + name: + '[layerNormalization] Throw if the bias data type doesn\'t match input data type', + input: {dataType: 'float32', dimensions: [1, 2, 3, 4]}, + options: { + scale: {dataType: 'float32', dimensions: [3, 4]}, + bias: {dataType: 'float16', dimensions: [3, 4]}, + axes: [2, 3], + }, + }, + { + name: + '[layerNormalization] Throw if the scale data type doesn\'t match input data type', + input: {dataType: 'float32', dimensions: [1, 2, 3, 4]}, + options: { + scale: {dataType: 'float16', dimensions: [3, 4]}, + bias: {dataType: 'float32', dimensions: [3, 4]}, + axes: [2, 3], + }, + }, + { + name: + '[layerNormalization] Throw if the bias dimensions doesn\'t match axis dimensions.', + input: {dataType: 'float32', dimensions: [1, 2, 3, 4]}, + options: { + bias: { + dataType: 'float32', + dimensions: [3, 3, 4] + }, // for 4D input, default axes = [1,2,3] + }, + }, + { + name: + '[layerNormalization] Throw if the scale dimensions doesn\'t match axis dimensions.', + input: {dataType: 'float32', dimensions: [1, 2, 3, 4]}, + options: { + scale: { + dataType: 'float32', + dimensions: [3, 3, 4] + }, // for 4D input, default axes = [1,2,3] + }, + }, + { + name: + '[layerNormalization] Throw if the bias rank doesn\'t match axis rank.', + input: {dataType: 'float32', dimensions: [1, 2, 3, 4]}, + options: { + bias: { + dataType: 'float32', + dimensions: [1, 2, 3, 4] + }, // for 4D input, default axes = [1,2,3] + }, + }, + { + name: + '[layerNormalization] Throw if the scale rank doesn\'t match axis rank.', + input: {dataType: 'float32', dimensions: [1, 2, 3, 4]}, + options: { + scale: { + dataType: 'float32', + dimensions: [1, 2, 3, 4] + }, // for 4D input, default axes = [1,2,3] + }, + }, +]; + +tests.forEach( + test => promise_test(async t => { + const input = builder.input( + 'input', + {dataType: test.input.dataType, dimensions: test.input.dimensions}); + + if (test.options && test.options.bias) { + test.options.bias = builder.input('bias', { + dataType: test.options.bias.dataType, + dimensions: test.options.bias.dimensions + }); + } + if (test.options && test.options.scale) { + test.options.scale = builder.input('scale', { + dataType: test.options.scale.dataType, + dimensions: test.options.scale.dimensions + }); + } + + if (test.output) { + const output = builder.layerNormalization(input, test.options); + assert_equals(output.dataType(), test.output.dataType); + assert_array_equals(output.shape(), test.output.dimensions); + } else { + assert_throws_js( + TypeError, () => builder.layerNormalization(input, test.options)); + } + }, test.name)); diff --git a/tests/wpt/tests/webnn/validation_tests/leakyRelu.https.any.js b/tests/wpt/tests/webnn/validation_tests/leakyRelu.https.any.js index 6fc19b1f0d7..f250b0eda66 100644 --- a/tests/wpt/tests/webnn/validation_tests/leakyRelu.https.any.js +++ b/tests/wpt/tests/webnn/validation_tests/leakyRelu.https.any.js @@ -5,3 +5,31 @@ 'use strict'; validateInputFromAnotherBuilder('leakyRelu'); + +validateUnaryOperation( + 'leakyRelu', floatingPointTypes, /*alsoBuildActivation=*/ true); + +promise_test(async t => { + const options = {alpha: 0.02}; + const input = + builder.input('input', {dataType: 'float32', dimensions: [1, 2, 3]}); + const output = builder.leakyRelu(input, options); + assert_equals(output.dataType(), 'float32'); + assert_array_equals(output.shape(), [1, 2, 3]); +}, '[leakyRelu] Test building an operator with options'); + +promise_test(async t => { + const options = {alpha: 0.03}; + builder.leakyRelu(options); +}, '[leakyRelu] Test building an activation with options'); + +promise_test(async t => { + const options = {alpha: Infinity}; + const input = builder.input('input', {dataType: 'float16', dimensions: []}); + assert_throws_js(TypeError, () => builder.leakyRelu(input, options)); +}, '[leakyRelu] Throw if options.alpha is Infinity when building an operator'); + +promise_test(async t => { + const options = {alpha: -NaN}; + assert_throws_js(TypeError, () => builder.leakyRelu(options)); +}, '[leakyRelu] Throw if options.alpha is -NaN when building an activation'); diff --git a/tests/wpt/tests/webnn/validation_tests/linear.https.any.js b/tests/wpt/tests/webnn/validation_tests/linear.https.any.js index 99c1daad3f1..6ec0389fc36 100644 --- a/tests/wpt/tests/webnn/validation_tests/linear.https.any.js +++ b/tests/wpt/tests/webnn/validation_tests/linear.https.any.js @@ -5,3 +5,31 @@ 'use strict'; validateInputFromAnotherBuilder('linear'); + +validateUnaryOperation( + 'linear', floatingPointTypes, /*alsoBuildActivation=*/ true); + +promise_test(async t => { + const options = {alpha: 1.5, beta: 0.3}; + const input = + builder.input('input', {dataType: 'float32', dimensions: [1, 2, 3]}); + const output = builder.linear(input, options); + assert_equals(output.dataType(), 'float32'); + assert_array_equals(output.shape(), [1, 2, 3]); +}, '[linear] Test building an operator with options'); + +promise_test(async t => { + const options = {beta: 1.5}; + builder.linear(options); +}, '[linear] Test building an activation with options'); + +promise_test(async t => { + const options = {beta: -Infinity}; + const input = builder.input('input', {dataType: 'float16', dimensions: []}); + assert_throws_js(TypeError, () => builder.linear(input, options)); +}, '[linear] Throw if options.beta is -Infinity when building an operator'); + +promise_test(async t => { + const options = {alpha: NaN}; + assert_throws_js(TypeError, () => builder.linear(options)); +}, '[linear] Throw if options.alpha is NaN when building an activation'); diff --git a/tests/wpt/tests/webnn/validation_tests/matmul.https.any.js b/tests/wpt/tests/webnn/validation_tests/matmul.https.any.js index 6ce0d87ca1c..8db16242c9e 100644 --- a/tests/wpt/tests/webnn/validation_tests/matmul.https.any.js +++ b/tests/wpt/tests/webnn/validation_tests/matmul.https.any.js @@ -68,10 +68,17 @@ const tests = [ output: {dataType: 'float32', dimensions: [2, 3, 5]} }, { + name: '[matmul] Throw if the input data type is not floating point', + inputs: { + a: {dataType: 'uint32', dimensions: [2, 3, 4]}, + b: {dataType: 'uint32', dimensions: [2, 4, 5]} + } + }, + { name: '[matmul] Throw if data type of two inputs don\'t match', inputs: { a: {dataType: 'float32', dimensions: [2, 3, 4]}, - b: {dataType: 'int32', dimensions: [2, 4, 5]} + b: {dataType: 'float16', dimensions: [2, 4, 5]} } }, { diff --git a/tests/wpt/tests/webnn/validation_tests/pad.https.any.js b/tests/wpt/tests/webnn/validation_tests/pad.https.any.js index 11c6a8f7ef2..cc39bee4c0d 100644 --- a/tests/wpt/tests/webnn/validation_tests/pad.https.any.js +++ b/tests/wpt/tests/webnn/validation_tests/pad.https.any.js @@ -15,3 +15,73 @@ multi_builder_test(async (t, builder, otherBuilder) => { () => builder.pad(inputFromOtherBuilder, beginningPadding, endingPadding)); }, '[pad] throw if input is from another builder'); + +const tests = [ + { + name: + '[pad] Test with default options, beginningPadding=[1, 2] and endingPadding=[1, 2].', + input: {dataType: 'float32', dimensions: [2, 3]}, + beginningPadding: [1, 2], + endingPadding: [1, 2], + options: { + mode: 'constant', + value: 0, + }, + output: {dataType: 'float32', dimensions: [4, 7]} + }, + { + name: '[pad] Throw if building pad for scalar input.', + input: {dataType: 'float32', dimensions: []}, + beginningPadding: [], + endingPadding: [], + }, + { + name: + '[pad] Throw if the length of beginningPadding is not equal to the input rank.', + input: {dataType: 'float32', dimensions: [2, 3]}, + beginningPadding: [1], + endingPadding: [1, 2], + options: { + mode: 'edge', + value: 0, + }, + }, + { + name: + '[pad] Throw if the length of endingPadding is not equal to the input rank.', + input: {dataType: 'float32', dimensions: [2, 3]}, + beginningPadding: [1, 0], + endingPadding: [1, 2, 0], + options: { + mode: 'reflection', + }, + }, + { + name: '[pad] Throw if the padding of one dimension is too large.', + input: {dataType: 'float32', dimensions: [2, 3]}, + beginningPadding: [2294967295, 0], + endingPadding: [3294967295, 2], + options: { + mode: 'reflection', + }, + }, +]; + +tests.forEach( + test => promise_test(async t => { + const input = builder.input( + 'input', + {dataType: test.input.dataType, dimensions: test.input.dimensions}); + if (test.output) { + const output = builder.pad( + input, test.beginningPadding, test.endingPadding, test.options); + assert_equals(output.dataType(), test.output.dataType); + assert_array_equals(output.shape(), test.output.dimensions); + } else { + assert_throws_js( + TypeError, + () => builder.pad( + input, test.beginningPadding, test.endingPadding, + test.options)); + } + }, test.name)); diff --git a/tests/wpt/tests/webnn/validation_tests/reshape.https.any.js b/tests/wpt/tests/webnn/validation_tests/reshape.https.any.js index 435551b716a..67491fbc168 100644 --- a/tests/wpt/tests/webnn/validation_tests/reshape.https.any.js +++ b/tests/wpt/tests/webnn/validation_tests/reshape.https.any.js @@ -12,3 +12,68 @@ multi_builder_test(async (t, builder, otherBuilder) => { assert_throws_js( TypeError, () => builder.reshape(inputFromOtherBuilder, newShape)); }, '[reshape] throw if input is from another builder'); + +const tests = [ + { + name: '[reshape] Test with new shape=[3, 8].', + input: {dataType: 'float32', dimensions: [2, 3, 4]}, + newShape: [3, 8], + output: {dataType: 'float32', dimensions: [3, 8]} + }, + { + name: '[reshape] Test with new shape=[24], src shape=[2, 3, 4].', + input: {dataType: 'float32', dimensions: [2, 3, 4]}, + newShape: [24], + output: {dataType: 'float32', dimensions: [24]} + }, + { + name: '[reshape] Test with new shape=[1], src shape=[1].', + input: {dataType: 'float32', dimensions: [1]}, + newShape: [1], + output: {dataType: 'float32', dimensions: [1]} + }, + { + name: '[reshape] Test reshaping a 1-D 1-element tensor to scalar.', + input: {dataType: 'float32', dimensions: [1]}, + newShape: [], + output: {dataType: 'float32', dimensions: []} + }, + { + name: '[reshape] Test reshaping a scalar to 1-D 1-element tensor.', + input: {dataType: 'float32', dimensions: []}, + newShape: [1], + output: {dataType: 'float32', dimensions: [1]} + }, + { + name: '[reshape] Throw if one value of new shape is 0.', + input: {dataType: 'float32', dimensions: [2, 4]}, + newShape: [2, 4, 0], + }, + { + name: + '[reshape] Throw if the number of elements implied by new shape is not equal to the number of elements in the input tensor when new shape=[].', + input: {dataType: 'float32', dimensions: [2, 3, 4]}, + newShape: [], + }, + { + name: + '[reshape] Throw if the number of elements implied by new shape is not equal to the number of elements in the input tensor.', + input: {dataType: 'float32', dimensions: [2, 3, 4]}, + newShape: [3, 9], + }, +]; + +tests.forEach( + test => promise_test(async t => { + const input = builder.input( + 'input', + {dataType: test.input.dataType, dimensions: test.input.dimensions}); + if (test.output) { + const output = builder.reshape(input, test.newShape); + assert_equals(output.dataType(), test.output.dataType); + assert_array_equals(output.shape(), test.output.dimensions); + } else { + assert_throws_js( + TypeError, () => builder.reshape(input, test.newShape)); + } + }, test.name)); diff --git a/tests/wpt/tests/webnn/validation_tests/slice.https.any.js b/tests/wpt/tests/webnn/validation_tests/slice.https.any.js index a45ecd3fcb5..de426216101 100644 --- a/tests/wpt/tests/webnn/validation_tests/slice.https.any.js +++ b/tests/wpt/tests/webnn/validation_tests/slice.https.any.js @@ -13,3 +13,69 @@ multi_builder_test(async (t, builder, otherBuilder) => { assert_throws_js( TypeError, () => builder.slice(inputFromOtherBuilder, starts, sizes)); }, '[slice] throw if input is from another builder'); + +const tests = [ + { + name: '[slice] Test with starts=[0, 1, 2] and sizes=[1, 2, 3].', + input: {dataType: 'float32', dimensions: [3, 4, 5]}, + starts: [0, 1, 2], + sizes: [1, 2, 3], + output: {dataType: 'float32', dimensions: [1, 2, 3]} + }, + { + name: '[slice] Throw if input is a scalar.', + input: {dataType: 'float32', dimensions: []}, + starts: [0], + sizes: [1] + }, + { + name: + '[slice] Throw if the length of sizes is not equal to the rank of the input tensor.', + input: {dataType: 'float32', dimensions: [3, 4, 5]}, + starts: [1, 2, 3], + sizes: [1, 1] + }, + { + name: + '[slice] Throw if the length of starts is not equal to the rank of the input tensor.', + input: {dataType: 'float32', dimensions: [3, 4, 5]}, + starts: [1, 2, 1, 3], + sizes: [1, 1, 1] + }, + { + name: + '[slice] Throw if the starting index is equal to or greater than input size in the same dimension.', + input: {dataType: 'float32', dimensions: [3, 4, 5]}, + starts: [0, 4, 4], + sizes: [1, 1, 1] + }, + { + name: '[slice] Throw if the number of elements to slice is equal to 0.', + input: {dataType: 'float32', dimensions: [3, 4, 5]}, + starts: [1, 2, 3], + sizes: [1, 0, 1] + }, + { + name: + '[slice] Throw if the ending index to slice is greater than input size in the same dimension.', + input: {dataType: 'float32', dimensions: [3, 4, 5]}, + starts: [0, 1, 2], + sizes: [3, 4, 1] + }, +]; + +tests.forEach( + test => promise_test(async t => { + const input = builder.input( + 'input', + {dataType: test.input.dataType, dimensions: test.input.dimensions}); + + if (test.output) { + const output = builder.slice(input, test.starts, test.sizes); + assert_equals(output.dataType(), test.output.dataType); + assert_array_equals(output.shape(), test.output.dimensions); + } else { + assert_throws_js( + TypeError, () => builder.slice(input, test.starts, test.sizes)); + } + }, test.name)); diff --git a/tests/wpt/tests/webnn/validation_tests/softplus.https.any.js b/tests/wpt/tests/webnn/validation_tests/softplus.https.any.js index 347dfcd9385..3cf91d26ecb 100644 --- a/tests/wpt/tests/webnn/validation_tests/softplus.https.any.js +++ b/tests/wpt/tests/webnn/validation_tests/softplus.https.any.js @@ -5,3 +5,6 @@ 'use strict'; validateInputFromAnotherBuilder('softplus'); + +validateUnaryOperation( + 'softplus', floatingPointTypes, /*alsoBuildActivation=*/ true); diff --git a/tests/wpt/tests/webnn/validation_tests/split.https.any.js b/tests/wpt/tests/webnn/validation_tests/split.https.any.js index 38f31266037..6f7809744a7 100644 --- a/tests/wpt/tests/webnn/validation_tests/split.https.any.js +++ b/tests/wpt/tests/webnn/validation_tests/split.https.any.js @@ -12,3 +12,83 @@ multi_builder_test(async (t, builder, otherBuilder) => { assert_throws_js( TypeError, () => builder.split(inputFromOtherBuilder, splits)); }, '[split] throw if input is from another builder'); + +const tests = [ + { + name: '[split] Test with default options.', + input: {dataType: 'float32', dimensions: [2, 6]}, + splits: [2], + outputs: [ + {dataType: 'float32', dimensions: [2, 6]}, + ] + }, + { + name: + '[split] Test with a sequence of unsigned long splits and with options.axis = 1.', + input: {dataType: 'float32', dimensions: [2, 6]}, + splits: [1, 2, 3], + options: {axis: 1}, + outputs: [ + {dataType: 'float32', dimensions: [2, 1]}, + {dataType: 'float32', dimensions: [2, 2]}, + {dataType: 'float32', dimensions: [2, 3]}, + ] + }, + { + name: '[split] Throw if splitting a scalar.', + input: {dataType: 'float32', dimensions: []}, + splits: [2], + }, + { + name: '[split] Throw if axis is larger than input rank.', + input: {dataType: 'float32', dimensions: [2, 6]}, + splits: [2], + options: { + axis: 2, + } + }, + { + name: '[split] Throw if splits is equal to 0.', + input: {dataType: 'float32', dimensions: [2, 6]}, + splits: [0], + options: { + axis: 2, + } + }, + { + name: + '[split] Throw if the splits can not evenly divide the dimension size of input along options.axis.', + input: {dataType: 'float32', dimensions: [2, 5]}, + splits: [2], + options: { + axis: 1, + } + }, + { + name: + '[split] Throw if the sum of splits sizes not equal to the dimension size of input along options.axis.', + input: {dataType: 'float32', dimensions: [2, 6]}, + splits: [2, 2, 3], + options: { + axis: 1, + } + }, +]; + +tests.forEach( + test => promise_test(async t => { + const input = builder.input( + 'input', + {dataType: test.input.dataType, dimensions: test.input.dimensions}); + if (test.outputs) { + const outputs = builder.split(input, test.splits, test.options); + assert_equals(outputs.length, test.outputs.length); + for (let i = 0; i < outputs.length; ++i) { + assert_equals(outputs[i].dataType(), test.outputs[i].dataType); + assert_array_equals(outputs[i].shape(), test.outputs[i].dimensions); + } + } else { + assert_throws_js( + TypeError, () => builder.split(input, test.splits, test.options)); + } + }, test.name)); diff --git a/tests/wpt/tests/webrtc-encoded-transform/tentative/RTCEncodedAudioFrame-metadata.https.html b/tests/wpt/tests/webrtc-encoded-transform/tentative/RTCEncodedAudioFrame-metadata.https.html index 1e420e6f725..609467b5e3e 100644 --- a/tests/wpt/tests/webrtc-encoded-transform/tentative/RTCEncodedAudioFrame-metadata.https.html +++ b/tests/wpt/tests/webrtc-encoded-transform/tentative/RTCEncodedAudioFrame-metadata.https.html @@ -76,7 +76,7 @@ promise_test(async t => { const original = result.value; let newMetadata = original.getMetadata(); newMetadata.rtpTimestamp = newMetadata.rtpTimestamp + 1; - let newFrame = new RTCEncodedAudioFrame(original, newMetadata); + let newFrame = new RTCEncodedAudioFrame(original, {metadata: newMetadata}); assert_not_equals(original.getMetadata().rtpTimestamp, newFrame.getMetadata().rtpTimestamp); assert_equals(newMetadata.rtpTimestamp, newFrame.getMetadata().rtpTimestamp); assert_equals(original.getMetadata().absCaptureTime, newFrame.getMetadata().absCaptureTime); @@ -117,7 +117,7 @@ promise_test(async t => { const original = result.value; let newMetadata = original.getMetadata(); newMetadata.synchronizationSource = newMetadata.synchronizationSource + 1; - assert_throws_dom("InvalidModificationError", () => new RTCEncodedAudioFrame(original, newMetadata)); + assert_throws_dom("InvalidModificationError", () => new RTCEncodedAudioFrame(original, {metadata: newMetadata})); resolve(); } }); diff --git a/tests/wpt/tests/webrtc-encoded-transform/tentative/RTCEncodedVideoFrame-metadata.https.html b/tests/wpt/tests/webrtc-encoded-transform/tentative/RTCEncodedVideoFrame-metadata.https.html index a2c684c1f15..77e1ed118f2 100644 --- a/tests/wpt/tests/webrtc-encoded-transform/tentative/RTCEncodedVideoFrame-metadata.https.html +++ b/tests/wpt/tests/webrtc-encoded-transform/tentative/RTCEncodedVideoFrame-metadata.https.html @@ -80,29 +80,29 @@ promise_test(async t => { promise_test(async t => { const senderReader = await setupLoopbackWithCodecAndGetReader(t, 'VP8'); const result = await senderReader.read(); - const metadata = result.value.getMetadata(); - metadata.rtpTimestamp = 100; - const newFrame = new RTCEncodedVideoFrame(result.value, metadata); + const frame_metadata = result.value.getMetadata(); + frame_metadata.rtpTimestamp = 100; + const newFrame = new RTCEncodedVideoFrame(result.value, {metadata: frame_metadata}); const newMetadata = newFrame.getMetadata(); // Encoding-related metadata. - assert_equals(newMetadata.frameId, metadata.frameId, 'frameId'); - assert_array_equals(newMetadata.dependencies, metadata.dependencies, + assert_equals(newMetadata.frameId, frame_metadata.frameId, 'frameId'); + assert_array_equals(newMetadata.dependencies, frame_metadata.dependencies, 'dependencies'); - assert_equals(newMetadata.width, metadata.width, 'width'); - assert_equals(newMetadata.height, metadata.height, 'height'); - assert_equals(newMetadata.spatialIndex, metadata.spatialIndex, + assert_equals(newMetadata.width, frame_metadata.width, 'width'); + assert_equals(newMetadata.height, frame_metadata.height, 'height'); + assert_equals(newMetadata.spatialIndex, frame_metadata.spatialIndex, 'spatialIndex'); - assert_equals(newMetadata.temporalIndex, metadata.temporalIndex, + assert_equals(newMetadata.temporalIndex, frame_metadata.temporalIndex, 'temporalIndex'); // RTP-related metadata. assert_equals(newMetadata.synchronizationSource, - metadata.synchronizationSource, 'synchronizationSource'); + frame_metadata.synchronizationSource, 'synchronizationSource'); assert_array_equals(newMetadata.contributingSources, - metadata.contributingSources, 'contributingSources'); - assert_equals(newMetadata.payloadType, metadata.payloadType, 'payloadType'); - assert_equals(newMetadata.rtpTimestamp, metadata.rtpTimestamp, 'rtpTimestamp'); + frame_metadata.contributingSources, 'contributingSources'); + assert_equals(newMetadata.payloadType, frame_metadata.payloadType, 'payloadType'); + assert_equals(newMetadata.rtpTimestamp, frame_metadata.rtpTimestamp, 'rtpTimestamp'); assert_not_equals(newMetadata.rtpTimestamp, result.value.getMetadata().rtpTimestamp, 'rtpTimestamp'); }, "[VP8] constructor with metadata carries over codec-specific properties"); diff --git a/tests/wpt/tests/workers/support/Worker-creation-happens-in-parallel.js.headers b/tests/wpt/tests/workers/support/Worker-creation-happens-in-parallel.js.headers new file mode 100644 index 00000000000..8249c49c340 --- /dev/null +++ b/tests/wpt/tests/workers/support/Worker-creation-happens-in-parallel.js.headers @@ -0,0 +1 @@ +Cross-Origin-Embedder-Policy:require-corp diff --git a/tests/wpt/tests/workers/support/Worker-postMessage-happens-in-parallel.js.headers b/tests/wpt/tests/workers/support/Worker-postMessage-happens-in-parallel.js.headers new file mode 100644 index 00000000000..8249c49c340 --- /dev/null +++ b/tests/wpt/tests/workers/support/Worker-postMessage-happens-in-parallel.js.headers @@ -0,0 +1 @@ +Cross-Origin-Embedder-Policy:require-corp |